Les décorateurs

Un décorateur est une fonction qui prend en paramètre une fonction et qui retourne une fonction. Ce type particulier de fonctions est également appelé une fonction d’ordre supérieur (higher order function). L’intérêt d’un décorateur est qu’il permet de transformer le comportement de la fonction passée en paramètre pour exécuter des traitements supplémentaires avant ou après les traitement normal de la fonction passée en paramètre.

Les décorateurs ont des usages multiples. Ils permettent de :

  • vérifier des conditions particulières lors de l’appel à une fonction

  • acquérir une ressource le temps de l’appel à la fonction (ouverture de fichier, accès à un service distant de base de données…)

  • tracer les appels de fonctions pour des raisons de débogage.

Nous verrons également qu’ils sont très utiles pour la programmation orientée objet.

Un premier décorateur

Prenons l’exemple d’un décorateur qui se contente d’écrire sur la sortie standard le début de l’appel à une fonction et la fin de l’appel :

def trace(func):
    def decorateur():
        print("Début d'appel à", func)
        func()
        print("Fin d'appel à", func)
    return decorateur

Il est possible maintenant de décorer un appel à une fonction :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def do_something():
    print("doing something")

do_something = trace(do_something)

do_something()
# Affiche
# Début d'appel à <function do_something at 0x7f210f642730>
# doing something
# Fin d'appel à <function do_something at 0x7f210f642730>

À la ligne 4, on décore la fonction do_something() en créant une nouvelle fonction que nous affectons directement à do_something pour changer le comportement initial de la fonction.

Les décorateurs agissent comme un composé de fonctions en mathématiques. Pour deux fonctions g(x) et f(x), cela équivaut à :

g O f(x)

En Python, le pie operator @ permet de réaliser la même opération de manière beaucoup plus simple :

1
2
3
4
5
6
7
8
9
@trace
def do_something():
    print("doing something")

do_something()
# Affiche
# Début d'appel à <function do_something at 0x7f210f642730>
# doing something
# Fin d'appel à <function do_something at 0x7f210f642730>

À la ligne 1, on décore la fonction do_something grâce à la notation @trace

Décorer des fonctions avec des paramètres et des valeurs de retour

Un décorateur est une fonction qui appelle la fonction décorée. Si cette dernière attend des paramètres et/ou retourne des valeurs, alors le décorateur doit en tenir compte :

def trace(func):
    def decorateur(*args, **kwargs):
        print("Début d'appel à", func)
        resultat = func(*args, **kwargs)
        print("Fin d'appel à", func)
        return resultat
    return decorateur
@trace
def moyenne(x, *args):
    """Calcule la moyenne d'un nombre quelconque de valeurs passées en paramètres."""
    nb = 1 + len(args)
    somme = x
    for y in args:
        somme += y
    return somme / nb

resultat = moyenne(5, 2, 2)
# Affiche
# Début d'appel à <function moyenne at 0x7f210a4a7a60>
# Fin d'appel à <function moyenne at 0x7f210a4a7a60>

print("La moyenne est", resultat)
# Affiche La moyenne est 3.0

Comme un décorateur est une simple fonction, il est possible de décorer des fonctions même sans utiliser le pie operator @. On peut donc décorer des méthodes natives de Python comme max()

max = trace(max)
resultat = max(1, 2, 3)
# Affiche
# Début d'appel à <built-in function max>
# Fin d'appel à <built-in function max>

print("Le maximum est", resultat)
# Affiche Le maximum est 3

Décorateur avec des paramètres

Il est possible de créer des décorateurs qui acceptent eux-mêmes des paramètres. En fait il suffit de créer une fonction qui retourne un décorateur. Ce dernier devant à son tour retourner la fonction de décoration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def repeter(nb_fois):
    def repeter_decorateur(func):
        def decorateur(*args, **kwargs):
            for i in range(nb_fois):
                func(*args, **kwargs)
        return decorateur
    return repeter_decorateur

@repeter(3)
def do_something():
    print("do something")

do_something()

# Affiche
# do something
# do something
# do something

Conserver la documentation malgré l’utilisation d’un décorateur

L’utilisation d’un décorateur n’est pas totalement transparente. En effet, le décorateur produit généralement une fonction qui replace la fonction d’origine. Reprenons l’exemple précédent :

def trace(func):
    def decorateur(*args, **kwargs):
        print("Début d'appel à", func)
        resultat = func(*args, **kwargs)
        print("Fin d'appel à", func)
        return resultat
    return decorateur

@trace
def moyenne(x, *args):
    """Calcule la moyenne d'un nombre quelconque de valeurs passées en paramètres."""
    nb = 1 + len(args)
    somme = x
    for y in args:
        somme += y
    return somme / nb

Si nous voulons afficher la document de la fonction moyenne() :

help(moyenne)

Nous verrons s’afficher :

Help on function decorateur in module __main__:

decorateur(*args, **kwargs)

En effet, la méthode moyenne() a été décorée. C’est donc la documentation de la méthode interne decorateur(*args, **kwargs) qui s’affiche et nous avons perdu la véritable documentation d’origine de la fonction moyenne().

Le module functools fournit un certain nombre de décorateur dont functools.wraps() qui permet de corriger ce problème :

from functools import wraps

def trace(func):
    @wraps(func)
    def decorateur(*args, **kwargs):
        print("Début d'appel à", func)
        resultat = func(*args, **kwargs)
        print("Fin d'appel à", func)
        return resultat
    return decorateur

Maintenant nous pouvons utiliser ce décorateur et la documentation de la méthode décorée est conservée :

@trace
def moyenne(x, *args):
    """Calcule la moyenne d'un nombre quelconque de valeurs passées en paramètres."""
    nb = 1 + len(args)
    somme = x
    for y in args:
        somme += y
    return somme / nb

help(moyenne)
# Affiche
# moyenne(x, *args)
#   Calcule la moyenne d'un nombre quelconque de valeurs passées en paramètres.

Appliquer plusieurs décorateurs

Il est possible d’ajouter plusieurs décorateurs sur une fonction :

@decorateur1
@decorateur2
def do_something():
    pass

Cela est équivalent à réaliser les appels suivants :

do_something = decorateur1(decorateur2(do_something))

L’ordre de déclaration des décorateurs est important puisqu’ils seront appliqués dans cet ordre.