Le terme décorateur vient du design pattern du même nom. Ce pattern consiste à modifier dynamiquement (au runtime) les caractéristiques d'un objet de code (fonction, classe, instance...). Cependant il si la parenté est évidente, il faut bien faire attention en Python de différencier le pattern decorator de la syntaxe du même nom. Il est en effet possible d'implémenter le pattern sans utiliser la syntaxe @xxxx.
La syntaxe en Python consiste donc à ajouter @mon_decorator avant la définition d'une fonction, d'une méthode, ou d'une classe où mon_decorator est un callable (souvent une fonction, mais pas forcément).
>>> def my_decorator(fn): ... do_stuff() ... return a_callable ... >>> @my_decorator ... def my_func(arg1,arg2): ... return arg1+arg2 ...
À l'import, lorsque my_func est évaluée, l'interpréteur Python appelle en fait my_func = my_decorator(my_func).
D'autres langages utilisent des concepts similaires. Je pense notamment ici aux annotations Java qui ont d'ailleurs inspiré la syntaxe @xxx de Python. Les annotations en Java fonctionnent de manière très différentes. En Java, par exemple les simples variables sont annotables. Les annotations peuvent aussi être évaluées a la compilation plutôt qu'à l'exécution.
Voici l'expression la plus simple d'un décorateur:
>>> def dummy(fn): ... print "into dummy(%s)"%(fn) ... return fn ... >>> @dummy ... def divide(arg1,arg2): ... print "into divide..." ... return arg1/arg2 ... into dummy(<function divide at 0x7f9edc8b62a8>) >>> print divide(1.,2.) into divide... 0.5
Comme nous le voyons, la fonction est décorée une fois pour toutes, mais notre décorateur ne sert pas à grand chose. Faisons un décorateur un peu plus utile. Le décorateur incargs par exemple incrémente de 1 les arguments passés à la fonction décorée :
>>> def incargs(fn): ... def increased_op(*args): ... print "into increased_op" ... args = [arg + 1. for arg in args] ... return fn(*args) ... return increased_op ... >>> @incargs ... def divide(arg1,arg2): ... print "into divide..." ... return arg1/arg2 ... into incargs >>> print divide(1.,2.) into increased_op into divide... 0.666666666667 >>> print divide(2.,3.) into increased_op into divide... 0.75
Comme nous le voyons, le fait que le décorateur renvoie une fonction imbriquée permet deux choses :
Nous avons déjà quelques outils qui nous permettent de décorer notre code, mais nous pouvons aller plus loin. Il est par exemple possible qu'on souhaite passer un argument à notre décorateur pour qu'il se comporte différemment suivant la fonction décorée. Nous allons ré-écrire incargs pour qu'on soit en mesure de lui passer en argument l'incrémentation qu'on désire.
Pour ce faire, il faut se rappeler qu'un décorateur est un callable, c'est à dire qu'il peut être une fonction, ou une instance d'une classe qui implémente la méthode __call__. Voyons d'abord le code puis son cheminement:
>>> # définition du décorateur >>> class xincargs(object): ... def __init__(self,inc): ... print "into Xincargs.__init__(self,%s)"%inc ... self.inc = inc ... def __call__(self,fn): ... print "into %s.__call__(%s)"%(self,fn) ... def do_incargs(*args,**kwargs): ... args = [arg + self.inc for arg in args] ... print "args are now: %s"%(args) ... return fn(*args) ... return do_incargs ... >>> @xincargs(3.) ... def add(arg1,arg2): ... print "into add" ... return arg1 + arg2 ... into xincargs.__init__(self,3.0) into <__main__.xincargs instance at 0x7ff5db987050>.__call__(<function add at 0x7ff5db9792a8>) >>> @xincargs(2.) ... def multiply(arg1,arg2): ... print "into multiply" ... return arg1 * arg2 ... into xincargs.__init__(self,2.0) into <__main__.xincargs instance at 0x7ff5db987bd8>.__call__(<function multiply at 0x7ff5db980398>) >>> print add(1,2) args are now: [4.0, 5.0] into add 9.0 >>> print multiply(1,2) args are now: [3.0, 4.0] into multiply 12.0
Tout ça est bien beau, mais ça reste abstrait. Quand est-ce que ça devient intéressant ? Un exemple très connu d'utilisation des décorateurs est la mise en place d'un cache des valeurs de retour d'une fonction. Le principe est d'ajouter à l'objet fonction un dictionnaire qui prend en clef un hachage des arguments de la fonction et en valeur le résultat pour ces arguments. Pour corser le tout, nous allons ajouter une durée de vie à chaque résultat mis en cache.
Cette durée de vie (TTL, d'après le nom anglais) sera variable selon la fonction décorée. Nous allons dons écrire une classe décorateur qui à laquelle nous passons le TTL. Le décorateur cherchera d'abord le résultat dans le cache, si ce résultat est suffisamment récent, il est renvoyé directement. S'il n'existe pas ou est trop vieux il est calculé, mis en cache puis renvoyé.
# -*- coding: utf-8 -*- import time, functools class xcache(object): def __init__(self,ttl): self.ttl = ttl def __call__(self,fn): # instanciation du cache fn.cache = {} # décoration de la fonction def cache_this(*args,**kwargs): # forgeage de la clef de cache if kwargs: # le frozenset permet de hasher le dictionaire kwargs key = args, frozenset(kwargs.argsiteritems()) else: key = args # fetch du cache cache = fn.cache if key in cache: result,birth = cache[key] if time.time() - birth < self.ttl: return result result = fn(*args, **kwargs) cache[key] = (result,time.time()) return result return functools.update_wrapper(cache_this, fn) @xcache(3) def nasty_heavy_function(arg1): print "doing dirt" time.sleep(2) return "done : %s"%arg1 start = time.time() print "%%%%%%%%%%%%%%%%%%%%%%%%%%" print "~2 secondes plus tard" print nasty_heavy_function(42) print ("%s"%(time.time() - start)) print "%%%%%%%%%%%%%%%%%%%%%%%%%%" print nasty_heavy_function(42) print "instantané" print ("%s"%(time.time() - start)) print "%%%%%%%%%%%%%%%%%%%%%%%%%%" print "encore 2 secondes, l'argument a changé..." print nasty_heavy_function("spam eggs") print ("%s"%(time.time() - start)) print "%%%%%%%%%%%%%%%%%%%%%%%%%%" time.sleep(1.5) print "instantané, le ttl de 'spam eggs' est encore bon " print nasty_heavy_function("spam eggs") print ("%s"%(time.time() - start)) print "%%%%%%%%%%%%%%%%%%%%%%%%%%" print "encore 2 secondes de calcul, le ttl pour 42 est dépassé" print nasty_heavy_function(42) print ("%s"%(time.time() - start )) print "%%%%%%%%%%%%%%%%%%%%%%%%%%"
Nous espérons que cet article vous aura permis de mieux comprendre le fonctionnement interne et les utilisations possibles des décorateurs. Il resterait beaucoup de choses à dire, parler des décorateurs de classe par exemple, mais chaque chose en son temps. Bon code !
Actions sur le document