Outils pour utilisateurs

Outils du site


decorateurs_modeles

Modèles de code pour développer des décorateurs

Problématique

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:

  • peut être écrit sous forme de fonction ou de classe (la version “classe” permettant de conserver des informations d'un appel à l'autre),
  • peut avoir ou pas des arguments, ce qui permet de modifier le comportement du même décorateur appliqué à plusieurs fonctions ou méthodes
  • peut modifier (envelopper) des fonctions ou des méthodes de classe

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.

Décorateurs sous forme de fonctions

Décorateurs sans arguments

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 

Décorateurs avec arguments

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 

Décorateurs sous forme de classes

Décorateurs sans arguments

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 

Décorateurs avec arguments

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 

Appel direct des méthodes du décorateur sous forme de classe

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.

Manipulation des arguments des fonctions décorées

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:

  • fonc: adresse de la fonction décorée
  • args: arguments par position
  • kwargs: arguments par défaut

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:

  • varsfn : liste des noms de variables par position et par défaut (ex: ['a', 'b', 'c', 'd', 'e'])
  • dico_varsfn_def : dictionnaire des variables passées par défaut (ex: {'e': 123, 'd': 'toto'})

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!

decorateurs_modeles.txt · Dernière modification: 2012/03/26 18:08 par tyrtamos

Outils de la page