Les décorateurs sont des fonctions ou des classes qui permettent de modifier des fonctions ou des méthodes par des opérations faites avant et/ou après leur appel.
Voir la documentation Python, et, par exemple: http://wiki.python.org/moin/PythonDecoratorLibrary
Schématiquement, un décorateur appliqué à une fonction entraine l'opération: fonction = decorateur(fonction), ce qui permet au décorateur “d'envelopper” la fonction à chacun de ses appels dans le code.
Un décorateur:
Il s'agit ici de créer un modèle de code “standard” afin de gagner du temps dans la construction d'un décorateur adapté au problème à résoudre.
C'est la forme la plus simple. C'est une fonction qui contient une sous-fonction:
import functools def decorateur(fonc): """ decorateur sans passage d'argument """ @functools.wraps(fonc) def appelfonc(*args, **kwargs): # instructions avant l'appel de fonction result = fonc(*args, **kwargs) # instructions après l'appel de fonction return result return appelfonc
Utilisation:
@decorateur def fonctiondecoree(): # ... return xxx
La ligne @functools.wraps(fonc) permet à la fonction décorée de conserver son nom (__name__) et son docstring (__doc__).
Dans l'exécution du décorateur, une fois la compilation réalisée, seule la fonction la plus interne appelfonc(*args, **kwargs) est exécutée à chaque appel de la fonction décorée.
Exemple d'application: afficher le temps d'exécution d'une fonction:
import functools import time def decorateur(fonc): """ decorateur affichant le temps d'exécution d'une fonction """ @functools.wraps(fonc) def appelfonc(*args, **kwargs): t = time.clock() # <= ligne ajoutée result = fonc(*args, **kwargs) t = time.clock()-t # <= ligne ajoutée print u"Temps: %.7f" % (t) # <= ligne ajoutée return result return appelfonc @decorateur def calcul(a,b,c): r = 0 for x in xrange(0,a): for y in xrange(0,b): for z in xrange(0,c): r += x*y*z return r print calcul(20,21,22) #Temps: 0.0011298 #9216900 print calcul(50,51,52) #Temps: 0.0149131 #2071046250
Et ça marche aussi pour décorer la méthode d'une classe:
class Test(object): @decorateur def methodedecoree(self, a, b, c): return a*b*c
Il est plus complet puisqu'on peut lui passer des arguments pour changer son comportement. Il est composé de 2 fonctions imbriquées dans la fonction principale.
import functools def decorateur(*dec_args, **dec_kwargds): """ decorateur avec passage d'arguments """ def _decorateur(fonc): @functools.wraps(fonc) def appelfonc(*args, **kwargs): # instructions avant l'appel de fonction result = fonc(*args, **kwargs) # instructions après l'appel de fonction return result return appelfonc return _decorateur
Utilisation:
@decorateur(<arguments passés au décorateur>) def fonctiondecoree(<variables de la fonction décorée>): # ... return xxx
Dans l'exécution du décorateur, une fois la compilation réalisée, seule la fonction la plus interne appelfonc(*args, **kwargs) est exécutée à chaque appel de la fonction décorée.
Comme le décorateur a des arguments passés, il DOIT avoir des parenthèses, même s'il est utilisé sans argument.
La ligne @functools.wraps(fonc) permet à la fonction décorée de conserver son nom (__name__) et son docstring (__doc__).
Exemple d'application: afficher le temps d'exécution d'une fonction exécutée n fois:
import functools import time def tempsexec(n=1): """ decorateur avec passage d'arguments """ def _decorateur(fonc): @functools.wraps(fonc) def appelfonc(*args, **kwargs): t = time.clock() for i in xrange(0,n): result = fonc(*args, **kwargs) t = time.clock()-t print u"Temps: %.7f" % (t) return result return appelfonc return _decorateur @tempsexec(100) def calcul(a,b,c): r = 0 for x in xrange(0,a): for y in xrange(0,b): for z in xrange(0,c): r += x*y*z return r print calcul(20,21,22) #Temps: 0.1080816 #9216900 print calcul(50,51,52) #Temps: 1.4949730 #2071046250
De même que le décorateur sans argument, ça marche aussi pour décorer la méthode d'une classe:
class Test(object): @decorateur(100) def methodedecoree(self, a, b, c): return a*b*c
C'est le même principe que le décorateur sous forme de fonction, mais la structure en classe permettra quelque chose d'intéressant: conserver des données entre les différents appels de la fonction décorée. On pourra aussi, si nécessaire, appeler une méthode en cours d'exécution pour, par exemple, remettre à zéro certains attributs.
import functools, types class decorateur(object): """ decorateur sans passage d'argument """ def __init__(self, fonc): """initialisation du décorateur""" self.fonc = fonc functools.wraps(fonc)(self) def __get__(self, inst, owner=None): """nécessaire pour décorer les méthodes: avoir le bon 'self' """ return types.MethodType(self, inst) def __call__(self, *args, **kwargs): """ méthode appelée à chaque appel de la fonction décorée """ # instructions avant result = self.fonc(*args, **kwargs) # instructions après return result
Utilisation:
@decorateur def fonctiondecoree(): # ... return xxx
Dans l'exécution du décorateur, une fois la compilation réalisée, seule la méthode __call__(self, *args, **kwargs) est exécutée à chaque appel de la fonction décorée.
La ligne functools.wraps(fonc)(self) permet à la fonction décorée de conserver son nom (__name__) et son docstring (__doc__).
La méthode __get__ est nécessaire quand on décore la méthode d'une classe pour avoir le bon 'self'
Exemple d'application: afficher le temps d'exécution moyen d'une fonction exécutée depuis le lancement du programme:
import functools import time class tempsexecmoyen(object): """ decorateur sans passage d'argument """ def __init__(self, fonc): self.fonc = fonc self.c = 0 # compteur du nombre d'exécution self.tt = 0 # temps total cumulé functools.wraps(fonc)(self) def __get__(self, inst, owner=None): """nécessaire pour décorer les méthodes: avoir le bon 'self' """ return types.MethodType(self, inst) def __call__(self, *args, **kwargs): """ méthode appelée à chaque appel de la fonction décorée """ t = time.clock() result = self.fonc(*args, **kwargs) t = time.clock()-t self.tt += t # cumul des temps self.c += 1 # incrémentation du compteur print u"Temps moyen: %.7f" % (self.tt/self.c) return result def resultat(self): """retourne le résultat: compteur d'appel et temps moyen d'exécution""" if self.c == 0: tm = 0 else: tm = self.tt/self.c return c, tm @tempsexecmoyen def calcul(a,b,c): """docstring de la fonction décorée""" r = 0 for x in xrange(0,a): for y in xrange(0,b): for z in xrange(0,c): r += x*y*z return r print calcul(20,21,22) #Temps moyen: 0.0011479 #9216900 print calcul(5,87,5) #Temps moyen: 0.0007639 #374100 print calcul(56,3,2) #Temps moyen: 0.0005465 #4620 print calcul(74,4,5) #Temps moyen: 0.0004805 #162060 print calcul(9,7,4) #Temps moyen: 0.0003946 #4536
J'ai ajouté la méthode 'resultat' au décorateur. On peut l'appeler à tout moment séparément de son effet sur l'exécution de la fonction décorée. Et on l'appelle par l'intermédiaire de la fonction décorée! cela donnera le nombre total d'appels ainsi que la moyenne des temps d'exécution cumulés sur tous les appels:
print calcul.resultat()
De même que le décorateur sous forme de fonction, ça marche aussi pour décorer la méthode d'une classe.
class Test(object): def __init__(self, x) self.x = x @decorateur def methodedecoree(self): print self.x
Même chose que le décorateur précédent, à part qu'on peut passer des arguments au décorateur pour déterminer son comportement devant chaque fonction décorée.
Par rapport aux code habituellement présenté comme modèle, j'ajoute une variable de classe adr qui est un dictionnaire qui va stocker les adresses des instances. Sans cette solution (je n'en ai pas trouvé d'autres), on ne peut pas comme précédemment accéder à une méthode supplémentaire du décorateur comme 'resultat'.
class decorateur(object): """ decorateur avec passage d'arguments """ adr = {} def __init__(self, *decargs, **deckwargs): self.decargs = decargs # arguments par position passés au décorateur self.deckwargs = deckwargs # arguments par défaut passés au décorateur def __call__(self, fonc): @functools.wraps(fonc) def appelfonc(*args, **kwargs): """ méthode appelée à chaque appel de la fonction décorée """ # instructions avant result = fonc(*args, **kwargs) # instructions après return result self.__class__.adr[fonc.__name__] = self return appelfonc def resultat(self): pass
@decorateur(<arguments passés au décorateur>) def fonctiondecoree(<variables de la fonction décorée>): # ... return xxx
Dans l'exécution du décorateur, une fois la compilation réalisée, seule la méthode appelfonc(*args, **kwargs) est exécutée, à chaque appel de la fonction décorée.
Comme le décorateur a des arguments passés, il DOIT avoir des parenthèses (même s'il est utilisé sans argument).
La ligne @functools.wraps(fonc) permet à la fonction décorée de conserver son nom (__name__) et son docstring (__doc__).
On lance l'exécution de la méthode 'resultat' du décorateur comme suit:
decorateur.adr['nom_de_la_fonction_décorée'].resultat()
De même que le décorateur sans argument, ça marche aussi pour décorer la méthode d'une classe:
class Test(object): @decorateur(100) def methodedecoree(self, a, b, c): return a*b*c
Il peut arriver qu'on veuille intervenir dans le décorateur sous forme de classe pour, par exemple, remettre à zéro certains attributs.
C'est facile: il suffit de se rappeler la définition du décorateur: fonctiondecoree = decorateur(fonctiondecoree). De ce fait, tous les attributs de la classe du décorateur se retrouvent adressées par le nom de la fonction décorée!
Exemple: réinitialisation d'un compteur d'appel, avec un décorateur sous forme de classe sans passage d'argument:
import functools class decorateur(object): """ decorateur sans passage d'argument """ def __init__(self, fonc): self.fonc = fonc self.compteur = 0 # simple compteur d'appels functools.wraps(fonc)(self) def __get__(self, obj, type): return self.__class__(self.f.__get__(obj, type)) def __call__(self, *args, **kwargs): """ méthode appelée à chaque appel de la fonction décorée """ # instructions avant self.compteur += 1 # incrémentation du compteur d'appels print self.compteur # et affichage de ce compteur result = self.fonc(*args, **kwargs) # instructions après return result def reinit(self): """remise à zéro du compteur d'appels""" self.compteur = 0 @decorateur def fonction(): pass fonction() fonction() fonction() fonction.reinit() # autre solution: fonction.compteur = 0 fonction() fonction() fonction()
Ce qui affiche:
1 2 3 1 2 3
On voit que le compteur a bien été remis à zéro.
Comme le décorateur peut intervenir avant chaque appel de la fonction décorée, il est intéressant de pouvoir intervenir sur les arguments passés à cette fonction
Exemple:
@mondecorateur def mafonction(a, b, c, d="toto", e=123): """docstring de mafonction""" # ... return a*b*c x = mafonction(4,5,6,d="truc")
Dans le décorateur, juste avant l'appel de la fonction décorée, on dispose donc des éléments suivants:
Avec l'exemple ci-dessus, on peut calculer dans le décorateur les éléments suivants.
Les éléments suivants sont connus dès que le décorateur est affecté à une fonction à décorer, et donc ne sont pas à recalculer à chaque appel de cette fonction:
# nom de la fonction décorée print fonc.func_name mafonction # docstring de la fonction décorée print fonc.func_doc docstring de mafonction
Méthode à intégrer dans la classe du décorateur:
def calculvars(self, fonc): """Calcul des variables passées à la fonction décorée""" # liste complète des variables passées à la fonction décorée varsfn = list(fonc.func_code.co_varnames[:fonc.func_code.co_argcount]) # liste des arguments déclarées par défaut argsfn_def = fonc.func_defaults if argsfn_def == None: argsfn_def = () # liste des variables par défaut varsfn_def = varsfn[len(varsfn)-len(argsfn_def):] # dictionnaire des variables par défaut avec leurs valeurs par défaut dico_varsfn_def = dict(zip(varsfn_def, argsfn_def)) # retour de la liste des var. et du dictionnaire des var. par défaut return varsfn, dico_varsfn_def
Cette méthode renvoie:
A chaque appel de la fonction décorée, on dispose en plus des arguments passés à cette fonction:
# tuple des arguments passés par position print args (7, 8, 9) # dictionnaire des arguments passés par défaut print kwargs {'d': 'machin'}
La variable kwargs ne contient que les arguments par défaut effectivement passés: ceux qui ne sont pas mentionnés à l'appel auront comme valeur la valeur par défaut déclarée avec la déclaration de la fonction (ici: e=123).
On a l'impression que args=(7, 8, 9) va correspondre avec varsfn_pos=['a', 'b', 'c'] et que kwargs={'d': 'machin'}, éventuellement complété, va correspondre à varsfn_def=['d', 'e'], mais ce n'est pas aussi simple, à cause de (grâce à ?) la souplesse de Python.
En effet, voilà 2 appels syntaxiquement corrects:
# appel dans lequel un argument par position (c=6) est passé avec son nom, comme un argument par défaut x = mafonction(4,5,c=6,d="truc") # appel dans lequel un argument par défaut (d="truc") est passé sans son nom, comme un argument par position x = mafonction(4,5,6,"truc")
Dans le 1er cas: args=(4,5) et kwargs={'c':6,'d':"truc"}
Dans le 2e cas: args=(4,5,6,"truc") et kwargs={}
Méthode à intégrer dans la classe du décorateur et calculant le dictionnaire de tous les arguments passés à la fonction décorée:
def calculargs(self, args, kwargs, varsfn, dico_varsfn_def): """Calcul du dictionnaire des arguments passés à la fonction décorée""" # initialisation du dico avec les arguments passés par position d = dict(zip(varsfn[:len(args)], args)) # on complète avec les arguments par défaut effectivement passés d.update(kwargs) # on complète avec les éventuels arguments par défaut NON passés for c in dico_varsfn_def.keys(): if c not in d.keys(): d[c] = dico_varsfn_def[c] # retour du dictionnaire return d
Exemple de valeur retournée: {'a': 4, 'c': 6, 'b': 5, 'e': 123, 'd': 'truc'}
Ce calcul fonctionne dans les 2 cas cités précédemment.
Avec un tel résultat, on peut provoquer des modifications des arguments passés, ou on peut trouver si on n'a pas déjà calculé avec ces arguments pour retrouver le résultat dans un cache.
Amusez-vous bien!