Décorateurs

Des fonctions qui enveloppent d’autres fonctions pour leur ajouter un comportement, par ex. journalisation ou contrôle d’accès. Notation @nom.

Sur cette page

Exemple

#
Python
import functools

def log_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Appel de {func.__name__}{args}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} a renvoyé {result!r}")
        return result
    return wrapper

@log_call
def award(name, exp):
    """Attribue des points d'expérience."""
    return f"{name}: +{exp} EXP"

award("Léa", 15)
print(award.__name__, "|", award.__doc__)
Résultatrésultat enregistré, vous pouvez le vérifier
Appel de award('Léa', 15)
award a renvoyé 'Léa: +15 EXP'
award | Attribue des points d'expérience.

Définition et utilisation

#

Un décorateur est une fonction qui reçoit une autre fonction et renvoie une version enrichie de celle-ci. Écrire @log_call au-dessus de la définition d’une fonction est un raccourci pour award = log_call(award). Le mécanisme fonctionne parce qu’en Python les fonctions sont des objets ordinaires : on peut les passer en argument et les renvoyer depuis d’autres fonctions.

Un décorateur typique définit à l’intérieur une fonction d’enrobage (wrapper). Elle accepte n’importe quels arguments grâce à *args et **kwargs, fait quelque chose avant d’appeler l’original, l’appelle, fait quelque chose après et renvoie son résultat. Vous pouvez ainsi ajouter de la journalisation, une mesure du temps, un contrôle d’accès ou des nouvelles tentatives sans toucher aux fonctions elles-mêmes.

Le wrapper masque le nom et la docstring de la fonction d’origine : sans étape supplémentaire, award.__name__ renverrait "wrapper". Le décorateur @functools.wraps(func) appliqué au wrapper copie ces informations, il vaut donc toujours la peine de l’utiliser. Un décorateur avec ses propres paramètres, par ex. @require_level(5), demande un niveau de plus : la fonction externe reçoit les paramètres et renvoie le véritable décorateur.

Plusieurs décorateurs au-dessus d’une même fonction s’appliquent de bas en haut : celui qui est le plus proche de la définition l’enveloppe en premier. La bibliothèque standard fournit de nombreux décorateurs prêts à l’emploi, par ex. @property, @classmethod, @dataclass ou @functools.lru_cache.

Syntaxe

#
Syntaxe
import functools

def decorateur(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        resultat = func(*args, **kwargs)
        return resultat
    return wrapper

@decorateur
def fonction():
    ...

Plus d’exemples

#
Décorateur avec paramètre
Python
import functools

def require_level(minimum):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(player, *args, **kwargs):
            if player["level"] < minimum:
                return f"{player['name']}: niveau {minimum} requis"
            return func(player, *args, **kwargs)
        return wrapper
    return decorator

@require_level(5)
def enter_world(player, world):
    return f"{player['name']} entre dans le monde {world}"

print(enter_world({"name": "Léa", "level": 7}, 3))
print(enter_world({"name": "Lucas", "level": 2}, 3))
Résultatrésultat enregistré, vous pouvez le vérifier
Léa entre dans le monde 3
Lucas: niveau 5 requis
Ordre de plusieurs décorateurs
Python
def bold(func):
    def wrapper():
        return f"**{func()}**"
    return wrapper

def exclaim(func):
    def wrapper():
        return f"{func()} !"
    return wrapper

@bold
@exclaim
def title():
    return "Nouveau niveau"

print(title())
Résultatrésultat enregistré, vous pouvez le vérifier
**Nouveau niveau !**

Cette écriture équivaut à title = bold(exclaim(title)) : le point d’exclamation est donc ajouté avant la mise en gras.

Un décorateur qui compte les appels
Python
import functools

def count_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@count_calls
def check_answer(answer):
    return answer == "B"

for answer in ["A", "B", "C"]:
    check_answer(answer)

print(check_answer.calls)
Résultatrésultat enregistré, vous pouvez le vérifier
3

Bonnes pratiques

#
  • Ajoutez toujours @functools.wraps(func) à la fonction d’enrobage. Vous conservez le nom et la docstring de l’original, ce qui facilite le débogage et la lecture des messages d’erreur.
  • Le wrapper doit accepter *args, **kwargs et renvoyer le résultat de l’original. Sans return, chaque fonction décorée se mettra à renvoyer None.
  • Le code du décorateur lui-même s’exécute une seule fois, à la définition de la fonction, alors que le wrapper s’exécute à chaque appel. Ce qui doit se produire à chaque appel va à l’intérieur du wrapper.

Entrées associées

#

Vous voyez une erreur ou il manque un exemple ? Écrivez-nous.