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'utilsie pas les parenthèses : # la fonction n'est pas apellé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, 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 fonction en Python est qu’on peut les définir à l’intérieur… d’une autre fonction.
def parler(): # On peut definir 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 appélé à l'intérieux 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'appele 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 : <function crier at 0xb7ea817c> # 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 finit ! Si on peut retourner une fonction, on peut aussi en passer une en paramètre…
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’éxé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ètres def decorateur_tout_neuf(fonction_a_decorer): # En interne, le décorateur définie une fonction à la volée: le wrapper. # Le wrapper va enrober la fonction orginale de telle sorte qu'il # puisse éxécuter du code avant et après celle-ci def wrapper_autour_de_la_fonction_originale(): # Mettre ici le code que l'on souhaite éxécuter AVANT que la # fonction s'éxécute print "Avant que la fonction ne s'éxécute" # Apperler la fonction (en utilisant donc les parenthèses) fonction_a_decorer() # Mettre ici le code que l'on souhaite éxécuter APRES que la # fonction s'éxécute print "Après que la fonction soit éxécutée" # Arrivé ici, la "fonction_a_decorer" n'a JAMAIS ETE EXECUTE # On retourne le wrapper qu'on l'on vient de créer. # Le wrapper contient la fonction originale et le code à éxé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 ton 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'éxécute #Je suis une fonction intouchable, on ne me modifie pas ! #Après que la fonction soit éxé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é par le décorateur :
une_fonction_intouchable = decorateur_tout_neuf(une_fonction_intouchable) une_fonction_intouchable() #output: #Avant que la fonction ne s'éxécute #Je suis une fonction intouchable, on ne me modifie pas ! #Après que la fonction soit éxécutée
Et c’est éxactement ce que les décorateurs font
Les décorateurs, démistifié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'éxécute #Me touche pas ! #Après que la fonction soit éxé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 “decorateur”.
Et bien sur, 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="--jambon--"): print food sandwich() #output: --jambon-- sandwich = pain(ingredients(sandwich)) sandwich() #output: #</''''''\> # #tomates# # --jambon-- # ~salade~ #<\______/>
Avec la syntaxe Python :
@pain @ingredients def sandwich(nouriture="--jambon--"): print nouriture sandwich() #output: #</''''''\> # #tomates# # --jambon-- # ~salade~ #<\______/>
Avec cet exemple, on voit aussi que l’ordre d’application des décorateurs a de l’importance :
@ingredients @pain def sandwich_zarb(nouriture="--jambon--"): print nouriture sandwich_zarb() #output: ##tomates# #</''''''\> # --jambon-- #<\______/> # ~salade~
Vous pouvez maintenant éteindre votre ordinateur et reprendre ou activité normale.
Aller à la partie 2.
No related posts.
Excellent le tuto !, parle nous des héritages un de ces jours, mais vas y mollo, c’est pas évident.
ça serait pas mal de faire un récapitulatif des modifs de syntaxe de la v3. C’est piégeux….à+ amigos.
Ca peut se faire, mais c’est un gros taff. L’héritage, c’est une notion de programmation générale, et pour quelqu’un qui le maitrise pas, c’est pas évident à digérer.
Super article !
Quand on les a compris, c’est quand meme magique les decorateurs.
Histoire d’etre sur, c’est par closure que quand on fait sandwich = deco(sandwich), la fonction decoratrice a acces a “l’ancien” code de sandwich (le code avant que sandwich ne fasse reference au wrapper) ?
A quand un petit tuto sur l’utilisation du mot cle with ? :)
Vu ce qu’il y a en francais dessus je suis sur que ca pourrait faire fureur ;)
Oui, c’est par closure.
Un article sur with ? Bonne idée.
Si je ne m’abuse,
creerParler
est une fabrique non ?Absolument.