Comprendre les décorateurs Python pas à pas (partie 1)


Les fonctions Python sont des objets

Pour comprendre les décorateurs, il faut d’abord comprendre que les fonctions sont des objets en Python. Cela a d’importantes conséquences:

def crier(mot="yes"):
    return mot.capitalize() + "!"
 
print(crier())
# output : 'Yes!'
 
# Puisque les fonctions sont des objets,
# on peut les assigner à des variables
 
hurler = crier
 
# Notez que l'on n’utilise pas les parenthèses :
# la fonction n'est pas appelée. Ici nous mettons la fonction "crier"
# dans la variable "hurler" afin de pouvoir appeler "crier" avec "hurler"
 
print(hurler())
# output : 'Yes!'
 
# Et vous pouvez même supprimer l'ancien nom "crier",
# la fonction restera accessible avec "hurler"
 
del crier
try:
    print(crier())
except NameError as e:
    print(e)
    #output: "name 'crier' is not defined"
 
print(hurler())
# output: 'Yes!'

Gardez ça à l’esprit, on va y revenir.

Une autre propriété intéressante des fonctions en Python est qu’on peut les définir à l’intérieur… d’une autre fonction.

def parler():
 
    # On peut définir une fonction à la volée dans "parler" ...
    def chuchoter(mot="yes"):
        return mot.lower()+"...";
 
    # ... et l'utiliser immédiatement !
 
    print(chuchoter())
 
# On appelle "parler", qui définit "chuchoter" A CHAQUE APPEL,
# puis "chuchoter" est appelé à l’intérieur de "parler"
 
parler()
# output:
# "yes..."
 
# Mais "chuchoter" N'EXISTE PAS en dehors de "parler"
 
try:
    print(chuchoter())
except NameError, e:
    print(e)
    #output : "name 'chuchoter' is not defined"

Passage des fonctions par référence

Toujours là ? Maintenant la partie amusante: vous avez vu que les fonctions sont des objets et peuvent donc:

  • être assignées à une variable;
  • être définies dans une autre fonction.

Cela veut dire aussi qu’une fonction peut retourner une autre fonction :-) Hop:

def creerParler(type="crier"):
 
    # On fabrique 2 fonctions à la volée
    def crier(mot="yes"):
        return mot.capitalize() + "!"
 
    def chuchoter(mot="yes") :
        return mot.lower() + "...";
 
    # Puis on retourne l'une ou l'autre
    if type == "crier":
        # on utilise pas "()", on n’appelle pas la fonction
        # on retourne l'objet fonction
        return crier
    else:
        return chuchoter
 
# Comment ce truc bizarre s'utilise ?
 
# Obtenir la fonction et l'assigner à une variable
parler = creerParler()
 
# "parler" est une variable qui contient la fonction "crier":
print(parler)
#output : <function crier at 0xb7ea817c>
 
# On peut appeler "crier" depuis "parler":
print(parler())
#ouput : YES!
 
# Et si on se sent chaud, on peut même créer et appeler la
# fonction en une seule fois:
print(creerParler("chuchoter")())
#output : yes...

Mais c’est pas fini ! Si on peut retourner une fonction, on peut aussi en passer une en argument…

def faireQuelqueChoseAvant(fonction):
    print("Je fais quelque chose avant d'appeler la fonction")
    print(fonction())
 
faireQuelqueChoseAvant(hurler)
#output:
#Je fais quelque chose avant d'appeler la fonction
#Yes!

C’est bon, vous avez toutes les cartes en main pour comprendre les décorateurs. En effet, les décorateurs sont des wrappers, c’est à dire qu’ils permettent d’exécuter du code avant et après la fonction qu’ils décorent, sans modifier la fonction elle-même.

Décorateur artisanal

Comment on en coderait un à la main:

# Un décorateur est une fonction qui attend une autre fonction en paramètre
def decorateur_tout_neuf(fonction_a_decorer):
 
    # En interne, le décorateur définit une fonction à la volée: le wrapper.
    # Le wrapper va enrober la fonction originale de telle sorte qu'il
    # puisse exécuter du code avant et après celle-ci
    def wrapper_autour_de_la_fonction_originale():
 
        # Mettre ici le code que l'on souhaite exécuter AVANT que la
        # fonction s’exécute
        print("Avant que la fonction ne s’exécute")
 
        # Apperler la fonction (en utilisant donc les parenthèses)
        fonction_a_decorer()
 
        # Mettre ici le code que l'on souhaite exécuter APRES que la
        # fonction s’exécute
        print("Après que la fonction se soit exécutée")
 
    # Arrivé ici, la "fonction_a_decorer" n'a JAMAIS ETE EXECUTEE
    # On retourne le wrapper que l'on vient de créer.
    # Le wrapper contient la fonction originale et le code à exécuter
    # avant et après, prêt à être utilisé.
    return wrapper_autour_de_la_fonction_originale
 
# Maintenant imaginez une fonction que l'on ne souhaite pas modifier.
def une_fonction_intouchable():
    print("Je suis une fonction intouchable, on ne me modifie pas !")
 
une_fonction_intouchable()
#output: Je suis une fonction intouchable, on ne me modifie pas !
 
# On peut malgré tout étendre son comportement
# Il suffit de la passer au décorateur, qui va alors l'enrober dans
# le code que l'on souhaite, pour ensuite retourner une nouvelle fonction
 
une_fonction_intouchable_decoree = decorateur_tout_neuf(une_fonction_intouchable)
une_fonction_intouchable_decoree()
#output:
#Avant que la fonction ne s’exécute
#Je suis une fonction intouchable, on ne me modifie pas !
#Après que la fonction se soit exécutée

Puisqu’on y est, autant faire en sorte qu’à chaque fois qu’on appelle une_fonction_intouchable, c’est une_fonction_intouchable_decoree qui est appelée à la place. C’est facile, il suffit d’écraser la fonction originale par celle retournée par le décorateur :

une_fonction_intouchable = decorateur_tout_neuf(une_fonction_intouchable)
une_fonction_intouchable()
#output:
#Avant que la fonction ne s’exécute
#Je suis une fonction intouchable, on ne me modifie pas !
#Après que la fonction se soit exécutée

Et c’est exactement ce que les décorateurs font.

Les décorateurs, démystifiés

L’exemple précédent, en utilisant la syntaxe précédente :

@decorateur_tout_neuf
def fonction_intouchable():
    print("Me touche pas !")
 
fonction_intouchable()
#output:
#Avant que la fonction ne s’exécute
#Me touche pas !
#Après que la fonction se soit exécutée

C’est tout. Oui, c’est aussi bête que ça.

@decorateur_tout_neuf est juste un raccourci pour

fonction_intouchable = decorateur_tout_neuf(fonction_intouchable)

Les décorateurs sont juste une variante pythonique du classique motif de conception “décorateur”.

Et bien sûr, on peut cumuler les décorateurs:

def pain(func):
    def wrapper():
        print("</''''''\>")
        func()
        print("<\______/>")
    return wrapper
 
def ingredients(func):
    def wrapper():
        print("#tomates#")
        func()
        print("~salade~")
    return wrapper
 
def sandwich(food="--jambon--"):
    print(food)
 
sandwich()
#output: --jambon--
sandwich = pain(ingredients(sandwich))
sandwich()
#output:
#</''''''\>
# #tomates#
# --jambon--
# ~salade~
#<\______/>

Avec la syntaxe Python :

@pain
@ingredients
def sandwich(nourriture="--jambon--"):
    print(nourriture)
 
sandwich()
#output:
#</''''''\>
# #tomates#
# --jambon--
# ~salade~
#<\______/>

Avec cet exemple, on voit aussi que l’ordre d’application des décorateurs a de l’importance :

@ingredients
@pain
def sandwich_zarb(nourriture="--jambon--"):
    print(nourriture)
 
sandwich_zarb()
#output:
##tomates#
#</''''''\>
# --jambon--
#<\______/>
# ~salade~

Vous pouvez maintenant éteindre votre ordinateur et reprendre une activité normale.

Aller à la partie 2.

19 thoughts on “Comprendre les décorateurs Python pas à pas (partie 1)

  • roro

    Excellent le tuto !, parle nous des héritages un de ces jours, mais vas y mollo, c’est pas évident.
    ça serait pas mal de faire un récapitulatif des modifs de syntaxe de la v3. C’est piégeux….à+ amigos.

  • Sam Post author

    Ca peut se faire, mais c’est un gros taff. L’héritage, c’est une notion de programmation générale, et pour quelqu’un qui le maitrise pas, c’est pas évident à digérer.

  • Flo

    Super article !
    Quand on les a compris, c’est quand meme magique les decorateurs.

    Histoire d’etre sur, c’est par closure que quand on fait sandwich = deco(sandwich), la fonction decoratrice a acces a “l’ancien” code de sandwich (le code avant que sandwich ne fasse reference au wrapper) ?

    A quand un petit tuto sur l’utilisation du mot cle with ? :)
    Vu ce qu’il y a en francais dessus je suis sur que ca pourrait faire fureur ;)

  • Sam Post author

    Oui, c’est par closure.

    Un article sur with ? Bonne idée.

  • Agagax

    Excellent, merci.

    Petit truc — ou alors j’ai pas compris ce qui est tout à fait plus que possible :

    titre : Passage des fonctions par référence

    # Comment ce truc bizarre s'utilise ?
    
    # Obtenir la fonction et l'assigner à une variable
    
    parler = creerParler()
    

    Dernière ligne : —> parler = creerParler sans les parenthèses, non ?

    Sinon, en vrac, vu que vous n’en prenez pas ombrage, bien au contraire :

    s/éxactement/exactement

    s/definir/définir

    s/appélé/appelé

    s/éxécut/exécut

    (pour tous les mots de la famille)

    s/elle même/elle-même

    s/salade/oignons <— c’est pas qu’il y ait une typo, c’est juste que je préfère les oignons.

  • Sam Post author

    Dernière ligne : —> parler = creerParler sans les parenthèses, non ?

    Nope, “parler” ne va pas contenir la fonction “creerParler”, mais la fonction fabriquée et retournée par “creerParler”. On appelle donc bien la fonction avec qu’elle s’exécute, qu’elle fabrique en interne la nouvelle fonction et la retourne.

    Merci pour les corrections.

  • Anne onyme

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

    * “‘chuchoter’ is not defined”*” -> “‘chuchoter’ is not defined”” (astérisque en trop);

    * “en passer une en paramètre” -> “en passer une en argument”, non? Cf. http://sametmax.com/la-difference-entre-parametres-et-arguments/;

    * “qui attend une autre fonction en paramètres” -> “qui attend une autre fonction en argument” (au singulier);

    * “le décorateur définie” -> “le décorateur définit”;

    * “JAMAIS ETE EXECUTE” -> “JAMAIS ETE EXECUTEE”;

    * “qu’on l’on vient” -> “que l’on vient”;

    * “Après que la fonction soit exécutée” -> “Après que la fonction se soit exécutée” (1 fois dans le code, 3 fois dans les commentaires);

    * “étendre ton comportement” -> “étendre son comportement”;

    * “Et c’est exactement ce que les décorateurs font” -> “Et c’est exactement ce que les décorateurs font.” (avec un point);

    * “démystifies” -> “démystifiés”;

    * “bien sur” -> “bien sûr”.

    Merci pour votre travail en tout cas!

  • Jérémie

    Au top le tuto! Je suis débutant en Python. On comprend et c’est bien vulgarisé

  • Gilles

    Chouette tuto les gars ! Riche et tout et tout …

    NB : “except NameError, e:” => ça plante (je suis en 3.61) par contre “except NameError as e:” ça marche très bien !

  • Alex

    Merci pour ce tuto (et tous les autres que je m’enfile allègrement depuis que je me suis mis à Python), il sont clairs et accessibles

  • NADIR

    un grand merci pour ce que vous faites, ca m’as vraiment aidé pour comprendre ce qui est le decorateur en tython !

Comments are closed.

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