Comprendre les décorateurs Python pas à pas (partie 2)
Dans la partie 1, nous avons vu comment fonctionnaient les décorateurs. Mais dans leur usage quotidien vous aller rencontrer des cas particuliers:
- Comment faire si la fonction décorée attend des arguments ?
- Comment changer le comportement d’un décorateur en lui passant des paramètres ?
- Comment préserver l’introspection ?
Introspection
Un des grands avantages de Python, c’est qu’il permet une très forte introspection, c’est à dire qu’on peut accéder à énormément d’informations sur le code lui-même.
Par exemple, si vous mettez une docstring à une fonction:
def ma_fonction(): """ C'est une super fonction """ pass
Vous pouvez ensuite récupérer la docstring très facilement:
>>> ma_fonction.__doc__ "\n C'est une super fonction\n "
Et vous pouvez la lire dans l’aide:
>>> help(ma_fonction) Help on function ma_fonction in module __main__: ma_fonction() C'est une super fonction (END)
L’autocompletion, la liste des attributs, le nom de la classe, etc. Toutes ces choses sont rendues accessibles grâce à l’introspection.
Mais quand vous décorez une fonction, vous l’enrobez dans une autre, détruisant ces informations:
def decorateur_inutile(func): def wrapper(): func() return wrapper @decorateur_inutile def ma_fonction(): """ C'est une super fonction """ pass >>> print ma_fonction.__doc__ None >>> help(ma_fonction) Help on function wrapper in module __main__: wrapper()
En effet, ma_fonction
contient maitenant wrapper
et non la fonction initiale. Heureusement le module functool
possède des outils pour y pallier.
Le plus utile est le décorateur @wraps
, qui copie littéralement toutes les infos d’une fonction sur son wrapper:
from functools import wraps def decorateur_inutile(func): @wraps(func) # il suffit de décorer le wrapper def wrapper(): func() return wrapper @decorateur_inutile def ma_fonction(): """ C'est une super fonction """ pass
Et tout s’arrange:
>>> ma_fonction.__doc__ "\n C'est une super fonction\n "
Fonction avec arguments
Jusqu’ici les fonctions que nous avons décorées n’attendaient pas d’arguments. Il faut en effet faire un petit effort supplémentaire pour les supporter.
# Pas de magie noire, c'est le wrapper qui passe l'argument: def un_decorateur_passant_un_argument(fonction_a_decorer): def un_wrapper_acceptant_des_arguments(arg1, arg2): print "J'ai des arguments regarde :", arg1, arg2 fonction_a_decorer(arg1, arg2) return un_wrapper_acceptant_des_arguments # Puisqu'on appelle en fait un_wrapper_acceptant_des_arguments(), # il accepte les arguments, et les passent à la fonctions décorée @un_decorateur_passant_un_argument def afficher_nom(nom, prenom): print "Mon nom est", nom, prenom afficher_nom("Peter", "Venkman") # output: #J'ai des arguments regarde : Peter Venkman #My name is Peter Venkman
Du coup pour décorer une méthode, il suffit d’accepter que le décorateur accepte self
. Le moyen le plus simple est encore d’accepter *args, **kwargs
, comme ça on est paré pour tous les cas.
Mais attention, si vous acceptez *args, **kwargs
, la liste des arguments ne sera plus disponible pour l’introspection. C’est quelque chose que @wraps
ne peut pas changer. La plupart du temps, c’est un compromis acceptable.
Passer un argument au décorateur lui-même
Le problème d’un décorateur, c’est qu’il doit accepter une fonction en paramètre. Pourtant, vous avez bien vu que @wraps
accepte lui même un argument. C’est qu’il existe donc un moyen de passer un argument au décorateur lui-même.
La solution est tordue: créer un décorateur à la volée. En fait ce decorateur ne sera plus le décorateur, mais le créateur de décorateur. Il y aura donc 3 niveaux d’imbrication… C’est partie pour une session de vaudou :
def createur_de_decorateur(): print ("Je fabrique des décorateurs. Je suis éxécuté une seule fois :" + "à la création du décorateur") def mon_decorateur(func): print "Je suis un décorateur, je suis éxécuté une seule fois quand on décore la fonction" def wrapper(): print ("Je suis le wrapper autour de la fonction décorée. " "Je suis appelé quand on appelle la fonction décorée. " "En tant que wrapper, je retourne le RESULTAT de la fonction décorée.") return func() print "En tant que décorateur, je retourne le wrapper" return wrapper print "En tant que créateur de décorateur, je retourne un décorateur" return mon_decorateur # Créons un décorateur, c'est juste une fonction après tout. nouveau_decorateur = createur_de_decorateur() #ouputs: #Je fabrique des décorateurs. Je suis éxécuté une seule fois : à la création du décorateur. #En tant que créateur de décorateur, je retourne un décorateur # Ensuite décorons la fonction def fonction_decoree(): print "Je suis la fonction décorée" fonction_decoree = nouveau_decorateur(fonction_decoree) #ouputs: #Je suis un décorateur, je suis éxécuté une seule fois quand on décore la fonction #En tant que décorateur, je retourne la fonction décorée # Appelons la fonction: fonction_decoree() #ouputs: #Je suis le wrapper autour de la fonction décorée. Je suis appelé quand on appelle la fonction décorée. #En tant que wrapper, je retourne le RESULTAT de la fonction décorée. #Je suis la fonction décorée
Aucune surprise ici. Faisons EXACTEMENT la même chose, mais en sautant les variables intermédiares.
def fonction_decoree(): print "Je suis la fonction décorée" fonction_decoree = createur_de_decorateur()(fonction_decoree) #ouputs: #Je fabrique des décorateurs. Je suis éxécuté une seule fois : à la création du décorateur. #En tant que créateur de décorateur, je retourne un décorateur #Je suis un décorateur, je suis éxécuté une seule fois quand on décore la fonction #En tant que décorateur, je retourne la fonction décorée. # Au final: fonction_decoree() #ouputs: #Je suis le wrapper autour de la fonction décorée. Je suis appelé quand on appelle la fonction décorée. #En tant que wrapper, je retourne le RESULTAT de la fonction décorée. #Je suis la fonction décorée
On recommence, en encore plus court::
@createur_de_decorateur() def fonction_decoree(): print "Je suis la fonction décorée" #ouputs: #Je fabrique des décorateurs. Je suis éxécuté une seule fois : à la création du décorateur. #En tant que créateur de décorateur, je retourne un décorateur #Je suis un décorateur, je suis éxécuté une seule fois quand on décore la fonction #En tant que décorateur, je retourne la fonction décorée. #Et pour finir: fonction_decoree() #ouputs: #Je suis le wrapper autour de la fonction décorée. Je suis appelé quand on appelle la fonction décorée. #En tant que wrapper, je retourne le RESULTAT de la fonction décorée. #Je suis la fonction décorée
Vous noterez qu’on a utilisé la notation @
, avec un appel de fonction: @createur_de_decorateur()
et non @createur_de_decorateur
!
Maintenant que nous pouvons générer des décorateurs à la volée, il suffit de passer des arguments au créateur de décorateur:
def createur_de_decorateur_avec_arguments(decorator_arg1, decorator_arg2): print "Je créé des décorateur et j'accepte des arguments:", decorator_arg1, decorator_arg2 def mon_decorateur(func): print "Je suis un décorateur, vous me passez des arguments:", decorator_arg1, decorator_arg2 # Ne pas mélanger les arguments du décorateurs et de la fonction ! def wrapped(function_arg1, function_arg2) : print ("Je suis le wrapper autour de la fonction décorée.\n" "Je peux accéder à toutes les variables\n" "\t- du décorateur: {0} {1}\n" "\t- de l'appel de la fonction: {2} {3}\n" "Et je les passe ensuite à la fonction décorée" .format(decorator_arg1, decorator_arg2, function_arg1, function_arg2)) return func(function_arg1, function_arg2) return wrapped return mon_decorateur @createur_de_decorateur_avec_arguments("Leonard", "Sheldon") def fonction_decoree_avec_arguments(function_arg1, function_arg2): print ("Je suis une fonctions décorée, je ne me soucie que de mes arguments: {0}" " {1}".format(function_arg1, function_arg2)) fonction_decoree_avec_arguments("Rajesh", "Howard") #output: #Je créé des décorateur et j'accepte des arguments: Leonard Sheldon #Je suis un décorateur, vous me passez des arguments: Leonard Sheldon #Je suis le wrapper autour de la fonction décorée function. #Je peux accéder à toutes les variables # - du décorateur: Leonard Sheldon # - de l'appel de la fonction: Rajesh Howard #Et je les passe ensuite à la fonction décorée #Je suis une fonctions décorée, je ne me soucie que de mes arguments: Rajesh Howard
mon_decorateur
a accès aux variables du scope supérieur car elles sont dans une closure. Vous ne pourrez donc pas les modifier.
Et voilà, un décorateur avec des arguments ! Les arguments peuvent être des
variables:
c1 = "Penny" c2 = "Leslie" @createur_de_decorateur_avec_arguments("Leonard", c1) def fonction_decoree_avec_arguments(function_arg1, function_arg2): print ("Je suis une fonctions décorée, je ne me soucie que de mes arguments:" " {0} {1}".format(function_arg1, function_arg2)) fonction_decoree_avec_arguments(c2, "Howard") #output: #Je créé des décorateur et j'accepte des arguments: Leonard Penny #Je suis un décorateur, vous me passez des arguments: Leonard Penny #Je suis le wrapper autour de la fonction décorée function. #Je peux accéder à toutes les variables # - du décorateur: Leonard Penny # - de l'appel de la fonction: Leslie Howard #Et je les passe ensuite à la fonction décorée #Je suis une fonctions décorée, je ne me soucie que de mes arguments: Leslie Howard
Comme vous le voyez, on peut passer des arguments au décorateur comme à n’importe quelle fonction en utilisant cette astuce. En fait on peut même utiliser *args, **kwargs
. Mais rappelez-vous: les décorateurs sont appelés uniquement une fois, au moment de l’import du script. On ne peut pas changer leurs arguments a posteriori. Quand vous faites from x import ma_fonction
, ma_fonction
est déjà décorée, et on ne peut rien y changer.
Super, mais ça sert à quoi un décorateur ?
Ca à l’air choutette et tout, mais un exemple d’usage concret, ça aiderait quand même….
Et bien il y a 1000 possibilités. Parmis les usages classiques:
- étendre la fonction d’une lib externe qu’on ne peut pas modifier;
- gérer les permissions d’une fonction;
- réagir aux arguments passés;
- débugger.
Le principe est la réutilisabilité: on fait un seul code, et on décore plein de fonctions avec.
Exemple:
def benchmark(func): """ Un décorateur qui affiche le temps qu'une fonction met à s'éxécuter """ import time def wrapper(*args, **kwargs): t = time.clock() res = func(*args, **kwargs) print func.__name__, time.clock()-t return res return wrapper def logging(func): """ Un décorateur qui log l'activité d'un script. (Ok, en vrai ça fait un print, mais ça pourrait logger !) """ def wrapper(*args, **kwargs): res = func(*args, **kwargs) print func.__name__, args, kwargs return res return wrapper def counter(func): """ Un compter qui compte et affiche le nombre de fonction qu'une fonction a été éxécutée """ def wrapper(*args, **kwargs): wrapper.count = wrapper.count + 1 res = func(*args, **kwargs) print "{0} a été utilisée: {1}x".format(func.__name__, wrapper.count) return res wrapper.count = 0 return wrapper @counter @benchmark @logging def reverse_string(string): return string[::-1] print reverse_string("Karine alla en Irak") print reverse_string("Sa nana snob porte de trop bons ananas") #output: #reverse_string ('Karine alla en Irak',) {} #wrapper 0.0 #wrapper a été utilisée: 1x #ablE sanana snob port ed etrop bons anan aS #reverse_string ('Sa nana snob porte de trop bons ananas',) {} #wrapper 0.0 #wrapper a été utilisée: 2x #sanana snob port ed etrop bons anan aS
Mais bien sur, le plus cool avec les décorateurs, c’est qu’on peut les utiliser immédiatement sans avoir à réécrire quoi ce que soit:
import httplib @counter @benchmark @logging def citation_de_futurama_au_hasard(): conn = httplib.HTTPConnection("slashdot.org:80") conn.request("HEAD", "/index.html") for key, value in conn.getresponse().getheaders(): if key.startswith("x-b") or key.startswith("x-f"): return value return "No, I'm ... doesn't!" print citation_de_futurama_au_hasard() print citation_de_futurama_au_hasard() #output: #citation_de_futurama_au_hasard () {} #wrapper 0.02 #wrapper a été utilisée: 1x #The laws of science be a harsh mistress. #citation_de_futurama_au_hasard () {} #wrapper 0.01 #wrapper a été utilisée: 2x #Curse you, merciful Poseidon!
Python vient chargé de décorateurs dans la lib standard: property
, staticmethod
, classmethod
, etc. Django gère les permissions des vues avec les décorateurs. Bottle déclare ses routes avec. Twisted donne l’impression qu’un appel asynchrone est synchrone en les utilisant. On peut faire vraiment tout et n’importe quoi.
Un grand merci à gawel, de l’AFPY, qui m’a, il y a quelques années, donné envie de découvrir les décorateurs.
No related posts.
Twister donne l’impression qu’un appel asynchrone est synchrone en les utilisant.
Merci, en plus ça me permet de corrigé une autre typo, car je voulais parler de twisted et non de twister.
Bonjour,
J’espère que malgré l’ancienneté du post vous lirez encore ce commentaire.
Dans d’autre post vous utilisez le décorateur “classmethod” (http://0bin.net/paste/457d639ee5d0d954ff5efc3aa1c0fcfc22582ad1#YcymIc9cyRkeffQsn/j4vqKKYMW8zkO4pJkCLh9zQao=) ça sert à quoi?
De même je ne connais pas les décorateurs “property” et “staticmethod”, ils servent à quoi?
merci
Bonjour @Takanuva,
Je pourrais faire un article là dessus pour plus de détails, mais en résumé:
- classmethod transforme la méthode en méthode de classe. On pas besoin d’instance pour éxécuter la méthode, et le premier paramètre est la classe elle même. On l’utilise pour le code commun à toutes les instances, et celles des classes enfants.
- staticmethod transforme la méthode en méthode statique. On as pas besoin d’instance pour éxécuter la méthode, et aucun paramètre n’est passé automatiquement à la méthode. On l’utilise pour le code de type “outil”, mais qui n’es pas particulièrement lié à la classe, pour des raisons d’encapsulation.
- property transforme la méthode en propriété, c’est à dire que la méthode est déguisée pour ressembler à un attribut, mais l’accès à cet attribut (avec le signe “=”) éxécute le code de la méthode. On l’utilise pour simplifier les APIs.
“#reverse_string (‘Karine alla en Irak’,) {}
#wrapper 0.0
#wrapper a été utilisée: 1x
#ablE sanana snob port ed etrop bons anan aS”
FAUX !
Limpide!
Le dernier lien n’est pas disponible. Merci WayBack :
Autrement, très instructif comme article !
Bien vu la wayback machine. Je rajoute le lien.
Je ne comprend pas comment tu peux faire ça dans la définition de ton wrapper:
wrapper.count = wrapper.count + 1
Comment peux-tu définir un attribut d’objet en dehors d’une classe?
(sinon, merci pour l’article, comme d’hab, clair et utile!)
Ah oui, et je me demandais aussi s’il existait une syntaxe en python qui permette de faire ça pour une exécution de fonction et pas sur une définition.
Je m’explique….euh comment dire…ben…imagine le décorateur “benchmark”, mais qui s’utiliserait comme le magic quote %timeit de ipython! ^^
Ce serait utile pour des script où on voudrait mesurer l’action de certaines actions sans avoir à placer des clock() partout…je sais pas si j’ai été clair!
Les fonctions sont des objets en Python, donc ça marche.
En fait, on ne déclare presque jamais des attributs en Python.
Quand tu fais dans un
__init__
:Tu ne déclare pas ton attribut.
Self
est déjà une instance (celle de l’objet en cours), et donc tu ne fais qu’attacher dynamiquement un attribut à une instance.Merci pour ta réponse Sam.
Mais tout est objet en python, donc dans ce cas, pourquoi j’ai une erreur en faisant:
a="nem"
a.zobi="la mouche"
AttributeError: 'str' object has no attribute 'zobi'
A la limite, ‘str’ est un type “spécial”, je peux comprendre….mais avec une fonction perso:
def test():
if not test.count: test.count = 0
else: test.count += 1
print test.count
AttributeError: 'function' object has no attribute 'count'
Dans l’exemple de ta réponse, je comprend très bien (bien que je na savais pas qu’on pouvait créer dynamiquement des attributs) car il s’agit d’une classe.
Mais pour une fonction, je comprend pas….en fait, c’est la notion “d’attribut de fonction” qui me turlupine (quel joli mot!).