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 : <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 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="--jambon--"): print(food) sandwich() #output: --jambon-- sandwich = pain(ingredients(sandwich)) sandwich() #output: #</''''''\> # #tomates# # --jambon-- # ~salade~ #<\______/> |
Avec la syntaxe Python :
@pain @ingredients def sandwich(nourriture="--jambon--"): print(nourriture) 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(nourriture="--jambon--"): print(nourriture) sandwich_zarb() #output: ##tomates# #</''''''\> # --jambon-- #<\______/> # ~salade~ |
Vous pouvez maintenant éteindre votre ordinateur et reprendre une activité normale.
Aller à la partie 2.
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.
et reprendre UNE activité normale.
Merci pour cet article d’une grande clareté !
Merci !
Excellent, merci.
Petit truc — ou alors j’ai pas compris ce qui est tout à fait plus que possible :
titre : Passage des fonctions par référence
Dernière ligne : —> parler = creerParler sans les parenthèses, non ?
Sinon, en vrac, vu que vous n’en prenez pas ombrage, bien au contraire :
s/éxactement/exactement
s/definir/définir
s/appélé/appelé
s/éxécut/exécut
(pour tous les mots de la famille)
s/elle même/elle-même
s/salade/oignons <— c’est pas qu’il y ait une typo, c’est juste que je préfère les oignons.
Nope, “parler” ne va pas contenir la fonction “creerParler”, mais la fonction fabriquée et retournée par “creerParler”. On appelle donc bien la fonction avec qu’elle s’exécute, qu’elle fabrique en interne la nouvelle fonction et la retourne.
Merci pour les corrections.
Comme pour les autres dépoussiérages, voici les quelques erreurs que j’ai repérées:
* “‘chuchoter’ is not defined”*” -> “‘chuchoter’ is not defined”” (astérisque en trop);
* “en passer une en paramètre” -> “en passer une en argument”, non? Cf. http://sametmax.com/la-difference-entre-parametres-et-arguments/;
* “qui attend une autre fonction en paramètres” -> “qui attend une autre fonction en argument” (au singulier);
* “le décorateur définie” -> “le décorateur définit”;
* “JAMAIS ETE EXECUTE” -> “JAMAIS ETE EXECUTEE”;
* “qu’on l’on vient” -> “que l’on vient”;
* “Après que la fonction soit exécutée” -> “Après que la fonction se soit exécutée” (1 fois dans le code, 3 fois dans les commentaires);
* “étendre ton comportement” -> “étendre son comportement”;
* “Et c’est exactement ce que les décorateurs font” -> “Et c’est exactement ce que les décorateurs font.” (avec un point);
* “démystifies” -> “démystifiés”;
* “bien sur” -> “bien sûr”.
Merci pour votre travail en tout cas!
Merci pour le tien, ça nous aide beaucoup.