Un décorateur pour accepter les callbacks en Python


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

3 thoughts on “Un décorateur pour accepter les callbacks en Python

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.