Les décorateurs sont apparus dans Python comme un sucre syntaxique à partir de la version 2.4 pour faciliter le travail avec des fonctions de premier ordre - celles qui acceptent ou retournent d'autres fonctions. L'évolution des approches pour étendre la fonctionnalité des fonctions a conduit à un format concis et expressif - des annotations via @decorator.
Dans les grands projets, il est souvent nécessaire de modifier ou d'entourer des fonctions avec une logique supplémentaire : journalisation, vérification d'accès, mise en cache, mesure du temps d'exécution. Sans les décorateurs, il fallait appeler manuellement les fonctions enveloppantes, ce qui gonflait le code.
Les décorateurs permettent d'extraire des aspects répétitifs dans des enveloppes distinctes, améliorant ainsi la lisibilité et la réutilisabilité du code. Avec @decorator, on peut ajouter élégamment des fonctionnalités aux fonctions et méthodes :
Exemple de code :
import time def timing_decorator(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) print(f'Elapsed: {time.time() - start:.3f}s') return result return wrapper @timing_decorator def slow_function(): time.sleep(0.5) slow_function() # Affiche combien de temps a pris l'exécution
Caractéristiques clés :
Les décorateurs peuvent-ils modifier la signature de la fonction enveloppée ?
On pense souvent à tort que la signature de la fonction par le décorateur ne change pas. En réalité, sans l'utilisation du module functools.wraps, les métadonnées disparaissent, ce qui peut provoquer des erreurs inattendues dans la documentation automatique ou l'introspection.
Exemple de code :
from functools import wraps def decorator(func): @wraps(func) def wrapper(*args): return func(*args) return wrapper
Les décorateurs peuvent-ils être paramétrés ?
On répond souvent que non. En réalité, il est possible de créer un décorateur paramétré via un niveau d'imbrication supplémentaire - une fonction qui retourne un décorateur.
Exemple de code :
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def hello(): print("Hello!")
Peut-on appliquer plusieurs décorateurs à une même fonction ? Quel sera l'ordre d'exécution ?
On pense souvent à tort que l'ordre n'a pas d'importance ou qu'il correspond à l'ordre de placement des décorateurs dans le code. En réalité, le décorateur le plus bas est appliqué en premier, puis le suivant, et ainsi de suite vers le haut.
Exemple de code :
def dec1(f): def wrapper(*a, **k): print("dec1") return f(*a, **k) return wrapper def dec2(f): def wrapper(*a, **k): print("dec2") return f(*a, **k) return wrapper @dec1 @dec2 def f(): print("core") f() # dec1, dec2, core
La journalisation du temps d'exécution a été ajoutée manuellement à 10 fonctions par copie de code.
Avantages :
Inconvénients :
Toute la logique de chronométrage a été extraites dans un décorateur, toutes les fonctions nécessitant cette métrique sont enveloppées avec le décorateur @timing_decorator.
Avantages :
Inconvénients :
functools.wraps, si on n'est pas attentif ;