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'utilsie pas les parenthèses :
# la fonction n'est pas apellé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 definir 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 appélé à l'intérieux 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'appele 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’éxé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 orginale de telle sorte qu'il
    # puisse éxécuter du code avant et après celle-ci
    def wrapper_autour_de_la_fonction_originale():
 
        # Mettre ici le code que l'on souhaite éxécuter AVANT que la
        # fonction s'éxécute
        print "Avant que la fonction ne s'éxécute"
 
        # Apperler la fonction (en utilisant donc les parenthèses)
        fonction_a_decorer()
 
        # Mettre ici le code que l'on souhaite éxécuter APRES que la
        # fonction s'éxécute
        print "Après que la fonction soit éxé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 à éxé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'éxécute
#Je suis une fonction intouchable, on ne me modifie pas !
#Après que la fonction soit éxé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'éxécute
#Je suis une fonction intouchable, on ne me modifie pas !
#Après que la fonction soit éxécutée

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

Les décorateurs, démistifié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'éxécute
#Me touche pas !
#Après que la fonction soit éxé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 “decorateur”.

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(nouriture="--jambon--"):
    print nouriture
 
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(nouriture="--jambon--"):
    print nouriture
 
sandwich_zarb()
#output:
##tomates#
#</''''''\>
# --jambon--
#<\______/>
# ~salade~

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

Aller à la partie 2.

No related posts.

flattr this!

6 comments

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

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

  3. 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 ;)

  4. Oui, c’est par closure.

    Un article sur with ? Bonne idée.

  5. Si je ne m’abuse, creerParler est une fabrique non ?

  6. Absolument.

Flux RSS des commentaires

Leave a Reply

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> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">

Jouer à mario en attendant que les autres répondent