Qu'est-ce qu'un décorateur ?
En Python, un décorateur est une fonction qui prend une autre fonction ou une classe en entrée et renvoie un objet enrichi. Il permet d'ajouter un comportement avant ou après l'exécution de la cible, ou de la remplacer, sans modifier sa définition originale. Syntaxiquement, il s'utilise avec le symbole @ placé juste avant la déclaration :
def etiqueter(func):
def interne(*args, **kwargs):
print(f"-- appel de {func.__name__} --")
return func(*args, **kwargs)
return interne
@etiqueter
def saluer(nom):
print(f"Bonjour, {nom}!")
saluer("Alice")
Ici, etiqueter encapsule saluer et affiche le nom de la fonction avant de l'executer. Cet exemple illustre le patron de conception wrapper.
Décorateurs paramétrés
Un décorateur peut lui-même accepter des arguments. Pour cela, il faut ajouter une niveau d'imbrication : une fonction extérieure reçoit les paramètres et retourne le vrai décorateur.
def annoncer(message_avant, message_apres):
def decorateur(func):
def enveloppe(*args, **kwargs):
print(message_avant)
valeur = func(*args, **kwargs)
print(message_apres)
return valeur
return enveloppe
return decorateur
@annoncer("Début du calcul", "Calcul terminé")
def addition(a, b):
return a + b
addition(3, 4)
annoncer n'est pas le décorateur final : c'est une usine à décorateurs qui retourne la fonction enveloppante adaptée aux messages choisis.
Fonctions de première classe : le fondement des décorateurs
Le mécanisme repose sur le fait que les fonctions Python sont des objets comme les autres. Elles peuvent être passées comme argument, retournées par une autre fonction ou assignées à des variables. Un décorateur n'est rien d'autre qu'une fonction qui reçoit une fonction et retourne une fonction.
def encadrer(func):
def nouvelle(*args, **kwargs):
print("Avant l'opération")
resultat = func(*args, **kwargs)
print("Après l'opération")
return resultat
return nouvelle
def multiplier(x, y):
return x * y
operation = encadrer(multiplier)
print(operation(2, 5))
Lors de l'écriture @encadrer, Python exécute en réalité multiplier = encadrer(multiplier). C'est cette substitution qui permet d'intercaler un traitement supplémentaire.
Préserver les métadonnées avec functools.wraps
Par défaut, le décorateur remplace le nom et la docstring de la fonction par ceux de l'enveloppe. Pour conserver l'identité de la fonction originale, on utilise functools.wraps.
import functools
def verifier(func):
@functools.wraps(func)
def enveloppe(*args, **kwargs):
print(f"Vérification avant {func.__name__}")
return func(*args, **kwargs)
return enveloppe
@verifier
def soustraire(x, y):
"""Retourne la différence entre x et y."""
return x - y
print(soustraire.__name__) # soustraire
print(soustraire.__doc__) # Retourne la différence entre x et y.
Sans @functools.wraps, ces deux attributs afficheraient ceux de la fonction interne enveloppe.
Applications concrètes
Journalisation des appels
Le décorateur suivant enregistre les entrées et sorties d'une fonction dans le journal applicatif :
import logging
import functools
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mon_application")
def tracer(func):
@functools.wraps(func)
def enveloppe(*args, **kwargs):
logger.info(f"Entrée dans {func.__name__} - args={args}, kwargs={kwargs}")
valeur = func(*args, **kwargs)
logger.info(f"Sortie de {func.__name__} -> {valeur}")
return valeur
return enveloppe
@tracer
def totaliser(prix, quantite):
return prix * quantite
totaliser(10, 3)
Mesure du temps d'exécution
On peut aussi créer un décorateur pour chronométrer une fonction :
import time
import functools
def chronometrer(func):
@functools.wraps(func)
def enveloppe(*args, **kwargs):
debut = time.perf_counter()
valeur = func(*args, **kwargs)
duree = time.perf_counter() - debut
print(f"{func.__name__} exécutée en {duree:.6f} seconde(s)")
return valeur
return enveloppe
@chronometrer
def attente():
time.sleep(0.5)
attente()
Chaînage de décorateurs
Python autorise l'application de plusieurs décorateurs sur une même fonction. L'ordre d'application est important : le décorateur le plus proche de la fonciton est appliqué en premier.
@tracer
@chronometrer
def calculer(a, b):
time.sleep(0.2)
return a ** b
calculer(2, 10)
Dans cet exemple, chronometrer encapsule d'abord calculer, puis tracer encapsule le résultat. L'équivalent explicite est calculer = tracer(chronometrer(calculer)).
Exemple avancé : enregistrement automatique
Un décorateur peut servir à enregistrer automatiquement des fonctions dans un registre. Cette technique est utile pour construire des systèmes de commandes, de routes ou de plugins.
def inscrire(registre, nom=None):
def decorateur(func):
cle = nom or func.__name__
registre[cle] = func
return func
return decorateur
commandes = {}
@inscrire(commandes, "somme")
def ajouter(x, y):
return x + y
@inscrire(commandes)
def produit(x, y):
return x * y
print(commandes)
print(commandes["somme"](2, 3))
Chaque fonction décorée est ajoutée au dictionnaire commandes sous le nom choisi ou son propre nom. Cela évite de maintenir manuellement une liste d'opérations disponibles.
Conseils d'utilisation
- Utilisez systématiquement
functools.wrapspour ne pas perdre les métadonnées. - Évitez d'introduire des effets de bord inattendus dans les décorateurs (chargement de configuration, connexions réseau, etc.).
- Lorsque le nombre de décorateurs augmente, envisagez des classes décorateurs ou des fonctions plus explicites pour maintenir la lisibilité.