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 allez 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 passe à 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
#Mon nom est 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 parti 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ée des décorateurs 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 fonction 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écorateurs 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 a l’air chouette et tout, mais un exemple d’usage concret, ça aiderait quand même….

Et bien il y a 1000 possibilités. Parmi 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):
    """
    Compte et affiche le nombre de fois qu'une fonction a été exé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"))
 
## reverse_string ('Karine alla en Irak',) {}
## wrapper 0.000132
## wrapper a été utilisée: 1x
## karI ne alla eniraK
## reverse_string ('Sa nana snob porte de trop bons ananas',) {}
## wrapper 0.000128
## 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 urllib
 
@counter
@benchmark
@logging
def citation_de_futurama_au_hasard():
    url = 'http://subfusion.net/cgi-bin/quote.pl?quote=futurama&number=1'
    try:
        res = urllib.request.urlopen(url)
        html = res.read().decode('ISO-8859-1')
        return html.split('<br>')[3].strip()
    except:
        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, @coroutine, @lru_cache, 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.

30 thoughts on “Comprendre les décorateurs Python pas à pas (partie 2)

  • Sam Post author

    Merci, en plus ça me permet de corrigé une autre typo, car je voulais parler de twisted et non de twister.

  • Sam Post author

    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.

  • JeromeJ

    “#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 !

  • Syl

    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!)

  • Syl

    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!

  • Sam Post author
    Les attributs en Python peuvent être attribués dynamiquement sur n'importe quel objet :
     
    In [1]: class Test:
       ...:     pass
       ...:
     
    In [2]: t = Test()
     
    In [3]: t.attribut = 'Syl'
     
    In [4]: t.attribut
    Out[4]: 'Syl'

    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__ :

    self.attribut.

    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.

  • Syl

    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!).

  • Georges Landa

    Bravo !
    Excellent tutorial sur les décorateurs en python !
    Remarquable de clarté !
    merci,
    Georges

  • Sam Post author

    @Syl: je suis désolé man, j’ai jamais répondu à ce com, et je le revoie juste maintenant. Bon, il est sans doute trop tard mais…

    Les deux examples ont deux raison différentes de ne pas fonctionner.

    Le premier c’est que les types de bases sont protégés contre les modifications. Quand je dis protégés, c’est pas non plus un coffre fort puisqu’avec un peu d’astuce on peut le faire, mais ça évite de le faire par erreur. C’est notament pour éviter des mauvaises pratiques comme le monkey patching des types de base qu’on peut voir en ruby et javascript et qui entrainent après des casse-tête de debug quand 2 libs le font.

    Le second exemple ne fonctionne pas car tu essaye de LIRE un attribut qui n’existe pas encore dans ton if. Ca ne marche sur AUCUN objet en Python. Par contre, tu peux parfaitement initialiser ton attribut à une valeur avant ta condition, et ça marchera. Les fonctions peuvent avoir des attributs arbitraires en Python, on s’en sert beaucoup dans les décorteurs, par exemple je l’utilise avec django quicky.

    @goearges : merci

  • Moato

    Hello je reprend ce qu’a essayer de dire Jerome: “ablE sanana snob port ed etrop bons anan aS” n’est pas l’inverse de “Karine alla en Irak”

  • Marc

    Salut,

    Je vois cette ligne:

    Vous noterez qu’on a utilisé la notation @, avec un appel de fonction: @createur_de_decorateur() et non @createur_de_decorateur !

    Salut , je ne comprends pas ce que ca change? j’ai essayé sans et ca na marche pas mais je ne comprends pas du tout pourquoi

    merci

  • Sam Post author
    @decorateur 
    def fonction()

    Va faire :

    fonction = decorateur(fonction)

    Alors que

    @decorateur()
    def fonction()

    Va faire :

    fonction = decorateur()(fonction)

    On a un niveau de plus d’imbrication de wrapper, et donc de fonctions dynamiquement créés. C’est comme inception, c’est plus compliqué quand on rajoute des couches.

    Note qu’un décorateur qui doit être appelé ne s’écrit pas pareil, puisqu’il doit retourner une fonction qui doit retourner une fonction.

  • Anb

    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.

    Compromis acceptable, sauf si l’on génère sa doc avec Sphinx.

    J’étais assez triste de voir toutes mes méthodes décorées prendre *args, **kwargs comme arguments du coup.

    Et puis je suis tombé sur ce snippet qui permet de dewrapper le wrapper (si on utilise functools.wraps):

    http://stackoverflow.com/a/28371786

    Ouf !

  • Oliverpool

    Un compter qui compte et affiche le nombre de fonction qu’une fonction

    Un peu trop de copier/coller ;-)

    Un compte*u*r qui compte et affiche le nombre de fo*is* qu’une fonction

    (d’ailleur “un compteur qui compte” c’est pas très orginal^^)

  • Anne Onyme

    Comme pour les autres dépoussiérages, voici les quelques erreurs que j’ai repérées:

    * “en lui passant des paramètres” -> “en lui passant des arguments” (cf. http://sametmax.com/la-difference-entre-parametres-et-arguments/);

    * “L’autocompletion” -> “L’autocomplétion”;

    * “contient maitenant” -> “contient maintenant”;

    * “Fonction avec arguments” -> “Fonction avec paramètres” (je sais, je fais chier)? Dans ce titre, on parle de la définition de la fonction, donc de paramètres, non?;

    * “J’ai des arguments regarde” -> “J’ai des arguments, regarde” (la virgule^^) (dans le code et dans les commentaires);

    * “à la fonctions décorée” -> “à la fonction décorée”;

    * “éxécuté” / “éxécuter” -> “exécuté” / “exécuter” (10 fois);

    * “En fait ce decorateur” -> “En fait ce décorateur”;

    * “quand on décore la )fonction” -> “quand on décore la fonction” (dans le code uniquement);

    * “Je créé des décorateur” -> “Je créé des décorateurs” (dans le code uniquement);

    * “les variables intermédiares” -> “les variables intermédiaires”;

    * “ça aiderait quand même….” -> “ça aiderait quand même…”;

    * “débugger” -> “déboguer”.

  • ast2

    Sur le fait de pouvoir ajouter dynamiquement un attribut à un objet, pourquoi ça ne marche pas sur une liste ?

    (ni sur un entier, float etc… apparemment ça ne marche que sur les objets ont on a crée soi-même la classe):

    L = [1, 8]

    L.test = ‘cest une liste’

    Traceback (most recent call last):

    File “<pyshell#10>”, line 1, in

    L.test = ‘cest une liste’

    AttributeError: ‘list’ object has no attribute ‘test’

  • ast2

    Ignorez ma dernière question, car la réponse est déjà dans les commentaires. J’aurais du tout lire avant d’envoyer. dsl

  • Antoine

    Pour l’exemple utilisant les décorateurs counter, benchmark, logging réécrits,

    on peut faire remonter l’information du nom de la fonction décorée à travers les décorateurs en utilisant systématiquement @wraps(func) dans chaque décorateur, ce qui peut donner:

    reverse_string (‘Karine alla en Irak’,) {}

    reverse_string a duré 0.0012250000000000039 secondes

    reverse_string a été utilisée: 1 fois

    karI ne alla eniraK

  • p4c

    Bonjour,

    Il y a un typo sur la fonction “print” dans la partie “Passer un argument au décorateur lui-même”:

    print(“Je suis un décorateur, je suis éxécuté une seule fois quand on décore la )fonction”

    Sinon merci pour le tuto.

  • JhonnyBoy56

    Je suis en stage d’informatique pour mon école d’ingé et je viens de découvrir ce site. Il est incroyable. Bravo.

    Cependant je lève une petite erreur sur le début de l’explication : @un_decorateur_passant_un_argument est doit print “Mon nom est” mais on retrouve “my name is” dans le output ! :o

    Sans doute un décorateur Reverso est passé par là ?

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.