Comprendre les décorateurs Python pas à pas (partie 1)

Les fonctions Python sont des objets

Pour comprendre les décorateurs, il faut d’abord comprendre que les fonctions sont des objets en Python. Cela a d’importantes conséquences:

def crier(mot="yes"):
    return mot.capitalize() + "!"
print(crier())
# output : 'Yes!'
# Puisque les fonctions sont des objets,
# on peut les assigner à des variables
hurler = crier
# Notez que l'on n’utilise pas les parenthèses :
# la fonction n'est pas appelée. Ici nous mettons la fonction "crier"
# dans la variable "hurler" afin de pouvoir appeler "crier" avec "hurler"
print(hurler())
# output : 'Yes!'
# Et vous pouvez même supprimer l'ancien nom "crier",
# la fonction restera accessible avec "hurler"
del crier
try:
    print(crier())
except NameError as e:
    print(e)
    #output: "name 'crier' is not defined"
print(hurler())
# output: 'Yes!'

Gardez ça à l’esprit, on va y revenir.
Une autre propriété intéressante des fonctions en Python est qu’on peut les définir à l’intérieur… d’une autre fonction.

def parler():
    # On peut définir une fonction à la volée dans "parler" ...
    def chuchoter(mot="yes"):
        return mot.lower()+"...";
    # ... et l'utiliser immédiatement !
    print(chuchoter())
# On appelle "parler", qui définit "chuchoter" A CHAQUE APPEL,
# puis "chuchoter" est appelé à l’intérieur de "parler"
parler()
# output:
# "yes..."
# Mais "chuchoter" N'EXISTE PAS en dehors de "parler"
try:
    print(chuchoter())
except NameError, e:
    print(e)
    #output : "name 'chuchoter' is not defined"

Passage des fonctions par référence

Toujours là ? Maintenant la partie amusante: vous avez vu que les fonctions sont des objets et peuvent donc:

  • être assignées à une variable;
  • être définies dans une autre fonction.

Cela veut dire aussi qu’une fonction peut retourner une autre fonction 🙂 Hop:

def creerParler(type="crier"):
    # On fabrique 2 fonctions à la volée
    def crier(mot="yes"):
        return mot.capitalize() + "!"
    def chuchoter(mot="yes") :
        return mot.lower() + "...";
    # Puis on retourne l'une ou l'autre
    if type == "crier":
        # on utilise pas "()", on n’appelle pas la fonction
        # on retourne l'objet fonction
        return crier
    else:
        return chuchoter
# Comment ce truc bizarre s'utilise ?
# Obtenir la fonction et l'assigner à une variable
parler = creerParler()
# "parler" est une variable qui contient la fonction "crier":
print(parler)
#output : 
# On peut appeler "crier" depuis "parler":
print(parler())
#ouput : YES!
# Et si on se sent chaud, on peut même créer et appeler la
# fonction en une seule fois:
print(creerParler("chuchoter")())
#output : yes...

Mais c’est pas fini ! Si on peut retourner une fonction, on peut aussi en passer une en argument…

def faireQuelqueChoseAvant(fonction):
    print("Je fais quelque chose avant d'appeler la fonction")
    print(fonction())
faireQuelqueChoseAvant(hurler)
#output:
#Je fais quelque chose avant d'appeler la fonction
#Yes!

C’est bon, vous avez toutes les cartes en main pour comprendre les décorateurs. En effet, les décorateurs sont des wrappers, c’est à dire qu’ils permettent d’exécuter du code avant et après la fonction qu’ils décorent, sans modifier la fonction elle-même.

Décorateur artisanal

Comment on en coderait un à la main:

# Un décorateur est une fonction qui attend une autre fonction en paramètre
def decorateur_tout_neuf(fonction_a_decorer):
    # En interne, le décorateur définit une fonction à la volée: le wrapper.
    # Le wrapper va enrober la fonction originale de telle sorte qu'il
    # puisse exécuter du code avant et après celle-ci
    def wrapper_autour_de_la_fonction_originale():
        # Mettre ici le code que l'on souhaite exécuter AVANT que la
        # fonction s’exécute
        print("Avant que la fonction ne s’exécute")
        # Apperler la fonction (en utilisant donc les parenthèses)
        fonction_a_decorer()
        # Mettre ici le code que l'on souhaite exécuter APRES que la
        # fonction s’exécute
        print("Après que la fonction se soit exécutée")
    # Arrivé ici, la "fonction_a_decorer" n'a JAMAIS ETE EXECUTEE
    # On retourne le wrapper que l'on vient de créer.
    # Le wrapper contient la fonction originale et le code à exécuter
    # avant et après, prêt à être utilisé.
    return wrapper_autour_de_la_fonction_originale
# Maintenant imaginez une fonction que l'on ne souhaite pas modifier.
def une_fonction_intouchable():
    print("Je suis une fonction intouchable, on ne me modifie pas !")
une_fonction_intouchable()
#output: Je suis une fonction intouchable, on ne me modifie pas !
# On peut malgré tout étendre son comportement
# Il suffit de la passer au décorateur, qui va alors l'enrober dans
# le code que l'on souhaite, pour ensuite retourner une nouvelle fonction
une_fonction_intouchable_decoree = decorateur_tout_neuf(une_fonction_intouchable)
une_fonction_intouchable_decoree()
#output:
#Avant que la fonction ne s’exécute
#Je suis une fonction intouchable, on ne me modifie pas !
#Après que la fonction se soit exécutée

Puisqu’on y est, autant faire en sorte qu’à chaque fois qu’on appelle une_fonction_intouchable, c’est une_fonction_intouchable_decoree qui est appelée à la place. C’est facile, il suffit d’écraser la fonction originale par celle retournée par le décorateur :

une_fonction_intouchable = decorateur_tout_neuf(une_fonction_intouchable)
une_fonction_intouchable()
#output:
#Avant que la fonction ne s’exécute
#Je suis une fonction intouchable, on ne me modifie pas !
#Après que la fonction se soit exécutée

Et c’est exactement ce que les décorateurs font.

Les décorateurs, démystifiés

L’exemple précédent, en utilisant la syntaxe précédente :

@decorateur_tout_neuf
def fonction_intouchable():
    print("Me touche pas !")
fonction_intouchable()
#output:
#Avant que la fonction ne s’exécute
#Me touche pas !
#Après que la fonction se soit exécutée

C’est tout. Oui, c’est aussi bête que ça.
@decorateur_tout_neuf est juste un raccourci pour

fonction_intouchable = decorateur_tout_neuf(fonction_intouchable)

Les décorateurs sont juste une variante pythonique du classique motif de conception « décorateur ».
Et bien sûr, on peut cumuler les décorateurs:

def pain(func):
    def wrapper():
        print("")
        func()
        print("<\______/>")
    return wrapper
def ingredients(func):
    def wrapper():
        print("#tomates#")
        func()
        print("~salade~")
    return wrapper
def sandwich(food="--champignons--"):
    print(food)
sandwich()
#output: --champignons--
sandwich = pain(ingredients(sandwich))
sandwich()
#output:
#
# #tomates#
# --champignons--
# ~salade~
#<\______/>

Avec la syntaxe Python :

@pain
@ingredients
def sandwich(nourriture="--champignons--"):
    print(nourriture)
sandwich()
#output:
#
# #tomates#
# --champignons--
# ~salade~
#<\______/>

Avec cet exemple, on voit aussi que l’ordre d’application des décorateurs a de l’importance :

@ingredients
@pain
def sandwich_zarb(nourriture="--champignons--"):
    print(nourriture)
sandwich_zarb()
#output:
##tomates#
#
# --champignons--
#<\______/>
# ~salade~

Vous pouvez maintenant éteindre votre ordinateur et reprendre une activité normale.
Aller à la partie 2.

Laissez un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *