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…
Ou alors, on peut utiliser une classe plutôt que d’attacher du state sur des fonctions ;-)
https://gist.github.com/remram44/31c1857417bbf124f57e
Et oui, une classe peut être utilisée comme décorateur. La bise !
Bonjour,
Il y a un bug sur la page http://sametmax.com/aller-plus-loin-en-python/
Le lien pour arriver ici est :
http://sametmax.com/un-decorateur-pour-accepter%20les-callbacks-en-python/
au lieu de :
http://sametmax.com/un-decorateur-pour-accepter-les-callbacks-en-python/
Sinon, super site : j’ai appris à faire du Python, ça change du Perl et ça me semble bien différent des mauvais souvenirs que j’ai du Java !
Merci.