Comprendre les décorateurs Python pas à pas (partie 1) | Sam & Max: Python, Django, Git et du cul

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 : 

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

  6 comments for “Comprendre les décorateurs Python pas à pas (partie 1)

  1. roro
    01/05/2012 at 23:18

    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. Sam
    02/05/2012 at 07:46

    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. Flo
    25/08/2012 at 23:41

    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. Sam
    26/08/2012 at 01:36

    Oui, c’est par closure.

    Un article sur with ? Bonne idée.

  5. Ryzz
    10/09/2013 at 08:08

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

  6. Sam
    01/10/2013 at 13:34

    Absolument.

Leave a Reply

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