Les décorateurs Python
Apprenez comment fonctionnent les décorateurs Python : écrire les vôtres, préserver les métadonnées avec functools.wraps, empiler les décorateurs et cas d'usage réels.
Un décorateur est une fonction qui enveloppe une autre fonction pour étendre ou modifier son comportement sans en changer le code source. Les décorateurs sont l'une des fonctionnalités les plus puissantes et idiomatiques de Python — ils sont le moteur derrière @staticmethod, @classmethod, @property, @functools.lru_cache, et de nombreux patterns de frameworks web populaires.
Cette page explique comment fonctionnent les décorateurs, comment écrire les vôtres de zéro, comment passer des arguments aux décorateurs, comment les empiler, et dans quel cas chaque pattern est le plus utile.
Comment fonctionnent les décorateurs
Un décorateur est simplement une fonction qui prend une autre fonction comme argument et retourne une nouvelle fonction. Python fournit la syntaxe @ comme raccourci pour en appliquer un :
@shout
def greet(name):
return f"hello, {name}"C'est exactement équivalent à :
def greet(name):
return f"hello, {name}"
greet = shout(greet)La ligne @shout indique à Python : après avoir défini greet, passez-la immédiatement à shout et reliez le nom greet à ce que shout retourne. À partir de ce moment, chaque appel à greet(...) passe d'abord par la logique de shout.
Écrire votre premier décorateur
Un décorateur définit normalement une fonction wrapper interne qui appelle la fonction originale et ajoute un comportement supplémentaire autour d'elle :
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet("python")) # HELLO, PYTHONwrapper accepte *args et **kwargs afin de transmettre toute combinaison d'arguments à func sans modification. Cela rend le décorateur compatible avec n'importe quelle fonction quelle que soit sa signature — une bonne habitude dès le départ.
Pourquoi le wrapper doit retourner la fonction interne
shout se termine par return wrapper, et non return wrapper(). C'est intentionnel : shout construit un nouveau callable, sans l'appeler encore. Si vous écriviez accidentellement return wrapper(), le décorateur s'exécuterait immédiatement au moment de la décoration et greet serait lié à la valeur de retour de wrapper — une chaîne — plutôt qu'au callable lui-même.
Préserver les métadonnées avec functools.wraps
Chaque fonction Python porte des métadonnées : __name__, __doc__, __module__, et davantage. Sans précaution particulière, un décorateur remplace la fonction originale par wrapper, perdant tout cela :
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet.__name__) # wrapper — wrong
print(greet.__doc__) # None — lostCorrigez cela en appliquant @functools.wraps(func) au wrapper. Il copie les métadonnées de la fonction originale sur wrapper :
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet.__name__) # greet
print(greet.__doc__) # Say hello to name.Utilisez toujours @functools.wraps dans tout décorateur que vous écrivez. Sans cela, les outils de débogage, les générateurs de documentation et les frameworks de test voient un nom de fonction incorrect. La seule exception est lorsque vous souhaitez intentionnellement masquer l'identité originale.
Exemples pratiques de décorateurs
Logger
Journaliser chaque appel à une fonction avec ses arguments et sa valeur de retour :
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 5)
# Calling add with args=(3, 5) kwargs={}
# add returned 8Timer
Mesurer le temps d'exécution d'une fonction :
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.6f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
total = slow_sum(1_000_000)
print(total) # slow_sum took 0.01xxs then 499999500000time.perf_counter() est le bon choix ici car il offre la résolution la plus élevée disponible pour les mesures de courte durée.
Mémoïsation (Cache)
Mettre en cache la valeur de retour pour chaque ensemble unique d'arguments afin que la fonction ne soit jamais calculée deux fois pour la même entrée :
import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55
print(fibonacci(30)) # 832040Pour le code en production, préférez le @functools.lru_cache ou @functools.cache intégré (Python 3.9+), qui gèrent les cas limites, la sécurité des threads et les limites de taille du cache. La version artisanale ci-dessus est utile pour comprendre le pattern.
Contrôle d'accès
Protéger une fonction pour qu'elle ne puisse s'exécuter que lorsqu'une condition est remplie :
import functools
def require_auth(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_authenticated"):
raise PermissionError("Authentication required.")
return func(user, *args, **kwargs)
return wrapper
@require_auth
def get_dashboard(user):
return f"Welcome, {user['name']}!"
guest = {"name": "Guest", "is_authenticated": False}
admin = {"name": "Admin", "is_authenticated": True}
try:
print(get_dashboard(guest))
except PermissionError as e:
print(e) # Authentication required.
print(get_dashboard(admin)) # Welcome, Admin!Décorateurs avec arguments
Parfois, vous avez besoin de configurer un décorateur au moment de la décoration — par exemple, pour répéter une fonction un nombre variable de fois. Les décorateurs simples ne peuvent pas prendre directement des arguments supplémentaires car Python passe la fonction, non les arguments. La solution est une fabrique de décorateurs : une fonction qui accepte la configuration et retourne un décorateur :
import functools
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say(message):
print(message)
say("hello")
# hello
# hello
# helloEn lisant de l'extérieur vers l'intérieur : @repeat(3) appelle d'abord repeat(3), qui retourne decorator. Python applique ensuite decorator à say, qui retourne wrapper. Ainsi say finit par pointer vers wrapper — même pattern qu'avant, avec le niveau supplémentaire juste pour amener n dans la portée.
L'imbrication peut sembler intimidante au début. Un raccourci mental : la fonction la plus externe contient la configuration, la fonction du milieu contient la fonction décorée, et la fonction la plus interne contient l'appel intercepté.
Empiler plusieurs décorateurs
Vous pouvez appliquer plusieurs décorateurs à une seule fonction en empilant des lignes @. Python les applique du bas vers le haut — le décorateur le plus proche du def est appliqué en premier :
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<b>" + func(*args, **kwargs) + "</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<i>" + func(*args, **kwargs) + "</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("Alice")) # <b><i>Hello, Alice</i></b>Équivalent à greet = bold(italic(greet)). italic enveloppe greet en premier, puis bold enveloppe le résultat. La sortie montre que italic s'exécute plus près de la chaîne brute et bold enveloppe l'extérieur.
Décorateurs basés sur des classes
Une classe peut aussi être un décorateur — tout objet doté d'une méthode __call__ est appelable. Les décorateurs basés sur des classes sont utiles lorsque le décorateur lui-même doit maintenir un état entre les appels :
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello()
say_hello()
print(say_hello.count) # 2functools.update_wrapper(self, func) fait le même travail que @functools.wraps — il copie les métadonnées de la fonction originale sur l'instance. Après la décoration, say_hello est une instance de CountCalls, donc say_hello.count est un accès à un attribut ordinaire.
Quand choisir une classe plutôt qu'un décorateur de fonction :
- Vous avez besoin d'un état persistant (
count,cache, indicateurs). - Le décorateur possède plusieurs méthodes ou une logique auxiliaire.
- Vous avez besoin que l'objet décoré soit introspectable en tant que type spécifique.
Pièges des décorateurs
Oublier d'appeler la fonction décorée
Une erreur courante au début est de retourner le wrapper mais d'oublier d'appeler func à l'intérieur :
def broken(func):
def wrapper(*args, **kwargs):
print("before")
# forgot to call func!
return wrapperLa fonction décorée retourne silencieusement None à chaque fois. Assurez-vous toujours que wrapper appelle func(*args, **kwargs) et retourne son résultat.
Décorer au mauvais niveau
Avec les décorateurs paramétrés, oublier l'appel externe est une erreur fréquente :
# Wrong — 'repeat' receives the function, not a count
@repeat # should be @repeat(3)
def say(msg):
print(msg)Cela passe say à repeat là où n est attendu, provoquant une TypeError lors de l'appel de say.
L'ordre des décorateurs est important
Avec des décorateurs empilés, l'ordre change le comportement. @timer puis @log_calls sur la même fonction chronométrera la version déjà journalisée, tandis que l'inverse journalisera la version déjà chronométrée. Réfléchissez à ce que vous souhaitez que chaque couche voie.
Relation avec les fermetures
La fonction wrapper d'un décorateur est une fermeture — elle capture func depuis la portée englobante et la maintient en vie même après que la fonction décorateur externe a retourné. Comprendre les fermetures rend les mécanismes internes des décorateurs évidents : l'objet cellule contenant func est exactement ce qui permet à wrapper d'appeler la fonction originale longtemps après que shout(greet) s'est terminé.
Pour la syntaxe *args et **kwargs utilisée dans les wrappers, consultez le chapitre dédié. Pour les expressions lambda qui s'associent bien aux décorateurs dans les patterns d'ordre supérieur, consultez le chapitre sur les lambda.
Référence rapide
| Pattern | Quand l'utiliser |
|---|---|
wrapper de base | Ajouter un comportement avant/après une fonction |
@functools.wraps | Toujours — préserve __name__, __doc__ |
| Fabrique de décorateurs (3 niveaux) | Besoin de configurer le décorateur |
| Décorateurs empilés | Composer plusieurs comportements indépendants |
| Décorateur basé sur une classe | Besoin d'un état persistant entre les appels |
@functools.lru_cache | Mémoïser des fonctions pures (intégré, prêt pour la production) |