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


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, 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 paramètre…

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ètres
def decorateur_tout_neuf(fonction_a_decorer):
 
    # En interne, le décorateur définie 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 soit exécutée"
 
    # Arrivé ici, la "fonction_a_decorer" n'a JAMAIS ETE EXECUTE
    # On retourne le wrapper qu'on 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 ton 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 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 soit exécutée

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

Les décorateurs, démystifies

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 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 sur, 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.

10 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.

Leave a comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

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