PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

Objets proxy et pattern adapter en Python

vendredi 16 août 2013 à 21:40

En informatique, le vocabulaire c’est une bonne partie du travail. On a des tas de termes comme polymorphisme, récursivité, idempotent ou closure. Certains sont des termes mathématiques, d’autres sont des anglicismes, mais la majorité sont juste des mots compliqués pour décrire des choses simples.

Vous connaissez mon manque d’attrait pour ça, on va donc clarifier.

Pour comprendre cet article, il vous faut les pré-requis suivants :

Et pour une fois, on va mettre un morceau qui colle bien au blog :

S’il vous plait, dessine moi un objet proxy

Un proxy, en anglais, c’est un mandataire, c’est à dire grosso merdo un intermédiaire. Un objet proxy est donc tout simplement un objet qui fait l’intermédiaire, généralement entre un objet et un autre.

Exemple, on a des enseignants qui vont avoir besoin d’une autorisation de sortie pour des enfants dans leur classe. Un enfant ne peut pas décider de cela, ses parents décident de ce genre de chose :

class Enseignant(object):
 
    # la demande d'autorisation attend un objet enfant en paramètre
    def demande_autorisation_de_sortie(self, enfant):
        return enfant.peut_sortir()
 
 
class Enfant(object):
 
    def __init__(self, age):
        self.age = age
 
    # mais un enfant ne peut pas donner cette information
    def peut_sortir(self):
        raise NotImplementedError('Un enfant ne peut decider de cela')
 
 
class Parent(object):
 
    # le parent va faire proxy sur l'objet enfant
    def __init__(self, enfant):
        self.enfant = enfant
 
    # et propose la même méthode que l'objet enfant afin de pouvoir
    # être passé en paramètre à sa place à la méthode
    # demande_autorisation_de_sortie()
    def peut_sortir(self):
        return self.enfant.age > 10

Ici, si la classe Enseignant appelle directement la méthode peut_sortir() de la classe Enfant, on obtient une erreur. :

>>> prof = Enseignant()
>>> enfant = Enfant(11)
>>> prof.demande_autorisation_de_sortie(enfant)
Traceback (most recent call last):
  File "<ipython-input-23-6d509f016e40>", line 1, in <module>
    prof.demande_autorisation_de_sortie(enfant)
  File "<ipython-input-18-e8d34178df4c>", line 4, in demande_autorisation_de_sortie
    return enfant.peut_sortir()
  File "<ipython-input-18-e8d34178df4c>", line 13, in peut_sortir
    raise NotImplementedError('Un enfant ne peut decider de cela')
NotImplementedError: Un enfant ne peut decider de cela

Nous avons créé un système où on est obligé de créer une instance de Parent, qui va se placer entre la classe Enseignant et la classe Enfant, et qui va retourner la valeur pour cette méthode :

>>> parent = Parent(enfant) # on passe l'enfant en paramètre au parent
>>> prof.demande_autorisation_de_sortie(parent) # et le parent remplace l'enfant
True

Ceci est un objet proxy, c’est à dire un objet qui se place entre deux objets, et fait l’intermédiaire. Pour que cela marche, il faut que l’objet proxy, ici l’instance de Parent, ait la même interface (les mêmes méthodes, avec les mêmes paramètres et noms) que l’objet derrière le proxy, ici l’instance de Enfant.

Dans notre cas, c’est académique, ça ne sert pas à grand chose. Mais il y a des tas de cas utiles pour les objets proxy.

Ainsi, on veut parfois créer des objets dit “lazy”, littéralement “paresseux”, c’est à dire pas évalués tout de suite. C’est ce que fait Django dans le cadre de certaines chaînes de caractères traduites. En effet, en Django, il existe une fonction lazy_gettex() que l’on peut appliquer sur les chaînes à traduire. Mais elle ne retourne pas une string, car on ne connait pas encore la langue dans laquelle il faut traduire la chaîne : la langue dépend de l’utilisateur qui viendra demander la page Web en question. lazy_gettex() retourne un objet proxy, et quand, à l’appel de la page Web, on tente de manipuler cet objet comme une string pour l’afficher, le proxy va chercher la traduction, et retourne la bonne chaîne. Pour que ça marche, il faut que l’interface de l’objet lazy ressemble trait pour trait à celui d’une chaîne.

Dans d’autres cas on veut mettre un proxy pour gérer des permissions, et selon le niveau de permission, l’objet proxy donne accès ou non à l’objet derrière lui.

Le design pattern décorateur, le fameux @truc en Python, n’est jamais qu’un objet fonction qui fait proxy vers une autre fonction qu’on a enrobé.

Mais la raison majeur pour laquelle on va utiliser un objet proxy est le design pattern “adapter”.

Ce n’est pas un chapeau, c’est un boa qui a avalé un éléphant

En informatique, on aime bien les traitements généralistes.

Par exemple, imaginez cet objet qui dit si on a le droit de se bourrer la gueule :

class Videur(object):
 
    def tu_peux_rentrer(self, personne):
        """
            Cette fonction n'est pas réaliste, puisque bien entendu une mineure
            à gros nichons rentrerait, et un arabe en short lacoste non,
            mais heureusement l'informatique est plus simple que notre société.
        """
        return personne.age >= 18
 
class Personne(object):
    def __init__(self, age):
        self.age = age

Et tant que vous avez la main sur le système, tout va bien dans le meilleur des mondes :

>>> import random
>>> clients = [Personne(random.randint(12, 65)) for x in xrange(random.randint(0, 100))]
>>> videur = Videur()
>>> for client in clients:
...     if videur.tu_peux_rentrer(client):
...         print("Glouglou")
...
Glouglou
Glouglou
Glouglou
Glouglou
Glouglou
...

Mais malheureusement la vie n’est pas toujours pleine de licornes et de bisounours, et votre collègue, lui, il a codé une lib en C qui a extrait les clients d’un XML, et quand vous vous mappez directement dessus avec ctype, vous obtenez des clients comme ça:

class Client(object):
    def __init__(self, majeur):
        self.majeur = majeur

Bon, vous vous dites que c’est pas grave. Après tout, c’est juste une condition à rajouter :

class Videur(object):
 
    def tu_peux_rentrer(self, personne):
 
        if hasattr(personne, 'age'):
            return personne.age >= 18
 
        return personne.majeur

Ouai mais votre commercial arrive avec un super contrat avec pillierdebar.com, et il faut s’interfacer avec leur API SOAP de 1997, dont la deserialisation vous retourne :

class Prospect(object):
    def __init__(self, datetime):
        self.date_de_naissance = datetime

Les connards. Ah les connards.

Mais bon, c’est pas grave, c’est juste une petite condition à rajouter.

import datetime
 
class Videur(object):
 
    def tu_peux_rentrer(self, personne):
 
        if hasattr(personne, 'age'):
            return personne.age >= 18
 
        if hasattr(personne, 'date_de_naissance'):
            # TODO: prendre en compte les années bisextiles
            return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18
 
        return personne.majeur

Bon, vous allez enfin pouvoir soufll…. Ahhhh ! Votre boss appelle, il a oublié de vous dire, le stagiaire travaille aussi sur une feature, et a fait son propre objet Prospect, c’est comme l’objet Client mais pas pareil. Non on ne peut pas changer. Oui il a fait de la merde mais si on change tout son code pête. Il se barre demain. Il n’a pas fait de test. Démerde toi.

class LeClient(object):
    def __init__(self, majeur_de_combien):
        self.__majeur_de_combien = majeur_de_combien
 
    def get_majeur_de_combien(self):
        return self.__majeur_de_combien

A ce stade là, le code est tellement moche de toute façon, vous vous en branlez complètement. Vous allez rentrer jouer Left4Dead avec un mod qui transforme les zombies pour qu’ils aient la tête de votre patron. Le reste n’est que détail d’implémentation.

class Videur(object):
 
    def tu_peux_rentrer(self, personne):
 
        if hasattr(personne, 'age'):
            return personne.age >= 18
 
        if hasattr(personne, 'date_de_naissance'):
            # TODO: prendre en compte les années bisextiles
            return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18
 
        if hasattr(personne, 'get_majeur_de_combien'):
            # retourne le nombre d'années après 18 ans ou -1. Don't ask...
            return personne.get_majeur_de_combien() > -1
 
        return personne.majeur

Quelque moi(s) plus tard, les specs changent. Il faut maintenant pouvoir vérifier la carte d’identité des clients. Ou la carte de membre. Et garder l’ancien système compatible bien entendu. Ah, et il y a aussi des controleurs, c’est comme des videurs, mais qui doivent vérifier si les personnes dans le bar sont majeurs, sauf qu’il n’acceptent pas la carte de membre ou la date de naissance donnée à l’oral.

class CarteDeMembre(object):
    pass
 
 
class CarteIdentité(object):
    def __init__(self, date_de_naissance):
        self.date_de_naissance = date_de_naissance
 
 
class Client(object):
    def __init__(self, majeur=None, carte_identite=None, carte_membre=None):
        self.majeur = majeur
        self.carte_identite = carte_identite
        self.carte_membre = None
 
 
class Videur(object):
 
    def tu_peux_rentrer(self, personne):
 
        if hasattr(personne, 'age') and personne.age is not None:
            return personne.age >= 18
 
        if hasattr(personne, 'carte_membre'):
            return True
 
        if hasattr(personne, 'carte_identite') and personne.carte_identite is not None:
            now = datetime.datetime.now()
            return (now - personne.carte_identite.date_de_naissance) / 365 > 18
 
        if hasattr(personne, 'date_de_naissance'):
            # TODO: prendre en compte les années bisextiles
            return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18
 
        if hasattr(personne, 'get_majeur_de_combien'):
            # retourne le nombre d'années après 18 ans ou -1. Don't ask...
            return personne.get_majeur_de_combien() > -1
 
        return personne.majeur
 
 
class Controleur(object):
 
    def est_autorise(self, personne):
 
        if hasattr(personne, 'age') and personne.age is not None:
            return personne.age >= 18
 
        if hasattr(personne, 'carte_identite') and personne.carte_identite is not None:
            now = datetime.datetime.now()
            return (now - personne.carte_identite.date_de_naissance) / 365 > 18
 
        if hasattr(personne, 'date_de_naissance'):
            # TODO: prendre en compte les années bisextiles
            return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18
 
        if hasattr(personne, 'get_majeur_de_combien'):
            # retourne le nombre d'années après 18 ans ou -1. Don't ask...
            return personne.get_majeur_de_combien() > -1
 
        return personne.majeur

Bon, là c’est la merde. Vous le sentez, demain il va y avoir un portail électronique qui check uniquement les cartes de membres. Quand il va falloir faire une modif quelque part, ça va être galère. La maintenance va être un enfer. Le debuggage, bien relou. Il faut trouver un moyen de faire plus propre.

C’est ici qu’entre en jeu le pattern “adapter”. Un adapter, c’est comme les adaptateurs pour prises, mais pour les objets. C’est un proxy qu’on met devant pour que la prise ressemble à une autre.

Dans le domaine du code, ça veut dire qu’on va enrober chaque objet dans un autre pour qu’ils aient tous la même interface. Le traitement du coup sera de nouveau très simple, et la complexité sera répartie dans les adapters.

 
# on met le code commun dans un parent abstrait, notamment le check pour
# savoir si un adapter convient a un objet donné
class Adapteur(object):
 
    cls = None
 
    def __init__(self, objet):
        self.objet = objet
 
    def peut_adapter(self, objet):
        return self.cls == type(objet)
 
 
# un adapter, c'est juste un objet proxy qui arrondit les angles
class PersonneAdapteur(Adapteur):
 
    cls = Personne
 
    # on va réduire la complexité en faisant en sorte que tous les checks
    # soient cachés derrière une simple property
    @property
    def majeur(self):
        return self.objet >= 18
 
 
class ProspectAdapteur(Adapteur):
 
    cls = Prospect
 
    @property
    def majeur(self):
        # TODO: prendre en compte les années bisextiles
        return (datetime.datetime.now() - self.objet.date_de_naissance) / 365 > 18
 
 
class LeClientAdapteur(Adapteur):
 
    cls = LeClient
 
    @property
    def majeur(self):
        # retourne le nombre d'années après 18 ans ou -1. Don't ask...
        return self.objet.get_majeur_de_combien() > -1
 
 
class ClientAdapteur(Adapteur):
 
    cls = Client
 
    # les adapteurs peuvent être configurables
    def __init__(self, objet, accepte_carte_membre=True):
        self.objet = objet
        self.accepte_carte_membre = accepte_carte_membre
 
 
    @property
    def majeur(self):
 
        if self.accepte_carte_membre and self.objet.carte_membre is not None:
            return True
 
        if self.objet.majeur:
            return True
 
        if self.object.carte_identite is not None:
            now = datetime.datetime.now()
            # TODO: prendre en compte les années bisextiles
            return (now - personne.carte_identite.date_de_naissance) / 365 > 18
 
        return False

Le controleur et le videur deviennent du coup beaucoup plus simples, maintenant que la logique est normalisée :

class VerificateurDeMajorite(object):
    def check_majorite(self, personne):
        for adapteur in self.adapteurs:
            if adapteur.peut_adapter(personne):
                return adapteur(personne).majeur
 
# du coup un vérificateur, c'est très déclaratif
class Videur(VerificateurDeMajorite):
 
    adapteurs = (
        PersonneAdapteur(),
        ClientAdapteur(),
        ProspectAdapteur(),
        LeClientAdapteur(),
    )
 
    def tu_peux_rentrer(self, personne):
        return self.check_majorite(personne)
 
 
class Controleur(VerificateurDeMajorite):
 
    adapteurs = (
        PersonneAdapteur(),
        ClientAdapteur(accepte_carte_membre=False),
        # ProspectAdapteur()
        LeClientAdapteur(),
    )
 
    def est_autorise(self, personne):
        return self.check_majorite(personne)

Si il faut ajouter un autre format de client, il suffit de coder un adapteur. Si il faut ajouter une autre classe de vérification, il suffit de lui donner les bons adapteurs.

Et le traitement est aussi simple, car si vous avez une liste qui contient plein de clients différents :

>>> clients = [Client(majeur=True), Client(carte_de_membre=CarteDeMembre()), Client(carte_identite=CarteIdentité(datetime.datetime.now()), LeClient(-1), Personne(age=16)...]

Vous pouvez quand même vérifier tout ça sans y réfléchir:

>>> videur = Videur()
>>> for client in clients:
...     if videur.tu_peux_rentrer(client):
...         print("Glouglou")
...
Glouglou
Glouglou
...
 
>>> controlleur = Controleur()
>>> for client in clients:
...     if not controlleur.est_autorise(client):
...         print("Ahhhh !")
...
Ahhhh !
Ahhhh !
...

Dans un cas comme celui de notre exemple, l’adapter est un peu overkill, on s’en tirerait aussi bien avec une liste de fonctions, et le code serait plus léger et lisible.

Mais quand les objets et la logique deviennent très complexes, tout ramener à une interface commune permet de simplifier énormément le code.

flattr this!

Le colonel moutarde dans la cuisine avec le chandelier

jeudi 15 août 2013 à 09:37

Ceci est un post invité de Golgotha posté sous licence creative common 3.0 unported.

On a parfois du mal à localiser l’origine d’un bug dans une longue requête sql ou un code spaghetti de 5000 lignes écris en 1999 par un biologiste recruté dans une SSII. Bien sur vous pouvez tenter la méthode traditionnelle, essayer de comprendre d’où viens le bug, de façon logique… ou bien gagner du temps et appliquer la méthode que je préfère dans ce genre de situation : la dichotomie.

Le principe est simple, couper en deux le code défectueux (commenter la moitié), re-faire un test, si le code marche, l’erreur viens de la partie commentée ou inversement, ensuite prenez la partie défectueuse et recommencez. Vous tomberez assez rapidement sur le bug, généralement en quelques minutes grand max, sans avoir réfléchi, c’est pratique quand vous devez trouver un bug rapidement en prod dans la panique générale.

flattr this!

Éviter que Sublime Text n’écrase les parenthèses fermantes

mercredi 14 août 2013 à 20:41

Quand on tape une parenthèse fermante (ou ] voire }) juste à côté d’une AUTRE parenthèse fermante, le comportement de Sublime Text par défaut est de ne pas ajouter de parenthèse, mais d’avancer d’un caractère.

La raison est que Sublime ferme automatiquement toute parenthèse ouverte, cette fonctionnalité est donc là pour vous éviter de taper une parenthèse fermante de trop.

Je déteste cette fonctionnalité.

Je suis assez grand pour savoir quand insérer ma parenthèse ou non, et je n’ai pas Parkinson.

Heureusement, comme tout dans cet éditeur qu’il est merveilleux, c’est désactivable. Ouvrez “Preferences > Key Bindings – User” et ajoutez ceci à la configuration :

    { "keys": ["\""], "command": "insert", "args": {"characters": "\""}, "context":
        [
            { "key": "setting.auto_match_enabled", "operator": "equal", "operand": true },
            { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true },
            { "key": "following_text", "operator": "regex_contains", "operand": "^\"", "match_all": true }
        ]
    },
    { "keys": [")"], "command": "insert", "args": {"characters": ")"}, "context":
        [
            { "key": "setting.auto_match_enabled", "operator": "equal", "operand": true },
            { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true },
            { "key": "following_text", "operator": "regex_contains", "operand": "^\\)", "match_all": true }
        ]
    },
    { "keys": ["'"], "command": "insert", "args": {"characters": "'"}, "context":
        [
            { "key": "setting.auto_match_enabled", "operator": "equal", "operand": true },
            { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true },
            { "key": "following_text", "operator": "regex_contains", "operand": "^'", "match_all": true }
        ]
    },
    { "keys": ["]"],"command": "insert", "args": {"characters": "]"}, "context":
        [
            { "key": "setting.auto_match_enabled", "operator": "equal", "operand": true },
            { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true },
            { "key": "following_text", "operator": "regex_contains", "operand": "^\\]", "match_all": true }
        ]
    },
    { "keys": ["}"], "command": "insert", "args": {"characters": "}"}, "context":
        [
            { "key": "setting.auto_match_enabled", "operator": "equal", "operand": true },
            { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true },
            { "key": "following_text", "operator": "regex_contains", "operand": "^\\}", "match_all": true }
        ]
    }

Abracadabra !

flattr this!

Déshabillez-vous pour l’occasion

mardi 13 août 2013 à 17:51

Se marrer avec le cul, c’est pas juste faire ça, dans sa chambre à deux sous la couette.

C’est se faire sucer dans un train parce que votre copine veut voir si elle à le temps de vous faire venir avant que le contrôleur passe.

C’est s’habiller en cuir SM et se faire chevaucher par sa colloc qui vous fouette avec une ceinture, juste pour voir la gueule du livreur de pizza halluciné quand vous lui ouvrez la porte avec la mise en scène (prendre le billet à 4 pattes entre les dents fait son petit effet).

C’est se foutre à poil avec un bonnet de Père Noël pour faire la bises aux 50 invités pakistanais de la house party de votre BFF anglaise parce qu’hier, bourré et voulant impressionner son pote sosie de Jim Morisson, vous lui aviez dit que “cap”.

Max fait des soirées mousse dans les camps nudistes, pas pour la baise (enfin si un peu), mais parce que c’est fun.

Je connais une meuf qui se met à genou pour servir de table à oualpé pas pour se faire baiser (il ne lui arrive rien), mais pour le fun.

D’ailleurs j’écris ce post à poil, parce qu’il est 15 h et que pour coder je n’ai pas besoin de vêtements.

Si ce n’est pas reproductif, c’est récréatif. Marrez-vous.

flattr this!

Un décorateur pour accepter les callbacks en Python

lundi 12 août 2013 à 12:03

Si vous lisez assidûment ce blog, et je n’en doute pas car il est génial, vous savez ce qu’est un décorateur et un callback. On a même vu comment créer un système de gestion d’événements en utilisant ces deux concepts.

Un des événements auxquels on veut réagir le plus souvent, c’est l’appel d’une fonction, donc en gros être capable de fournir un callback quand une fonction est appelée. On peut bien entendu coder la logique du callback dans chaque fonction et méthode que l’on met en œuvre, mais avec un peu d’astuce, on peut trouver une solution générique qui va couvrir Pareto des besoins.

L’idée, c’est donc de coder un décorateur :

def accept_callbacks(func):
 
    # on va stocker tous les callbacks là dedans
    callbacks = []
 
    @wraps(func)
    def wrapper(*args, **kwargs):
 
        # on appelle la fonction originale
        result = func(*args, **kwargs)
 
        # on appelle ensuite chaque callback en lui passant le resultat 
        # de la fonction ainsi que les paramètres qui avaient été passés
        for callback in callbacks:
            callback(result, *args, **kwargs)
 
        # et on retourne le résultat
        return result
 
    # on attache la liste des callbacks au wrapper pour y avoir accès depuis
    # l'extérieur
    wrapper.callbacks = callbacks
 
    return wrapper

Du coup, pour accepter des callbacks sur une fonction, il suffit de décorer la fonction :

@accept_callbacks
def add(a, b):
    return a + b

Ensuite on écrit son callback avec la signature qui va bien :

def mon_callback(result, a, b):
    print("Ma fonction a été appelée avec a=%s et b=%s !" % (a, b))
    print("Elle a retourné le résultat '%s'" % result)

Et pour ajouter un callback, c’est juste une insertion dans la liste :

add.callbacks.append(mon_callback)

Derrière, chaque appel de la fonction va appeler également tous les callbacks :

print(add(1, 1))
## Ma fonction a été appelée avec a=1 et b=1 !
## Elle a retourné le résultat '2'
## 2
 
print(add(42, 69))
## Ma fonction a été appelée avec a=42 et b=69 !
## Elle a retourné le résultat '111'
## 111
 
add.callbacks.remove(mon_callback)
 
print(add(0, 0))
# 0

Et le plus fun, c’est que ça marche aussi sans rien modifier avec les méthodes :

def autre_callback(result, self, truc_important):
    print("Ma fonction a été appelée avec truc_important=%s !" % truc_important)
    print("Elle a retourné '%s'" % result)
 
 
class CaMarcheAussiAvecUneClass(object):
 
    def __init__(self, repeat=1):
        self.repeat = repeat
 
    @accept_callbacks
    def puisque_une_classe_a_des_methodes(self, truc_important):
        return truc_important.upper() * self.repeat
 
 
CaMarcheAussiAvecUneClass.puisque_une_classe_a_des_methodes.callbacks.append(autre_callback)
 
instance1 = CaMarcheAussiAvecUneClass()
instance2 = CaMarcheAussiAvecUneClass(2)
 
print(instance1.puisque_une_classe_a_des_methodes("Le fromage de chèvre"))
## Ma fonction a été appelée avec truc_important=Le fromage de chèvre !
## Elle a retourné 'LE FROMAGE DE CHÈVRE'
## LE FROMAGE DE CHÈVRE
 
print(instance2.puisque_une_classe_a_des_methodes("Les perforeuses"))
## Ma fonction a été appelée avec truc_important=Les perforeuses !
## Elle a retourné 'LES PERFOREUSESLES PERFOREUSES'
## LES PERFOREUSESLES PERFOREUSES

Par contre, si on veut qu’un callback ne s’active que pour une instance donnée, alors il faut ruser un peu :

# le retrait d'un callback, c'est un simple retrait de la liste
CaMarcheAussiAvecUneClass.puisque_une_classe_a_des_methodes.callbacks.remove(autre_callback)
 
def callback_pour_une_instance(result, self, truc_important):
    # on check que l'instance est celle que l'on veut
    if self is instance1:
        print("Ma fonction a été appelée avec truc_important=%s !" % truc_important)
        print("Elle a retourné '%s'" % result)
 
CaMarcheAussiAvecUneClass.puisque_une_classe_a_des_methodes.callbacks.append(callback_pour_une_instance)
 
print(instance1.puisque_une_classe_a_des_methodes("Les points noirs des coccinelles"))
## Ma fonction a été appelée avec truc_important=Les points noirs des coccinelles !
## Elle a retourné 'LES POINTS NOIRS DES COCCINELLES'
## LES POINTS NOIRS DES COCCINELLES
 
print(instance2.puisque_une_classe_a_des_methodes("Les panneaux sens uniques"))
## LES PANNEAUX SENS UNIQUESLES PANNEAUX SENS UNIQUES

Niveau perf, ce n’est pas optimal, et bien sûr, l’appel des callbacks est synchrone et blocant. Ce n’est pas un souci dans 90 % des cas, pour les autres cas, vous devrez faire le truc à la main. En même temps, dès qu’on a des problèmes de perf, les codes génériques fonctionnent rarement.

Je vais peut être rajouter ça dans batbelt moi…


Télécharger le code de l’article

flattr this!

Error happened! 0 - count(): Argument #1 ($value) must be of type Countable|array, null given In: /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php:428 http://ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/?138 #0 /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php(999): VroumVroum_Blog->update() #1 /var/www/ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/index.php(1): require_once('...') #2 {main}