Tu as déjà vu le symbole @ en Python et tu t’es demandé ce qu’il faisait là, collé au-dessus d’une fonction ? Les décorateurs Python paraissent mystérieux… jusqu’au moment où tu comprends qu’ils ne sont qu’une façon élégante d’ajouter des comportements sans répéter de code. Dans cet article, tu vas apprendre quand les utiliser, comment ils fonctionnent vraiment, et surtout comment écrire les tiens proprement, en évitant les pièges courants.
Quand Et Pourquoi Utiliser Un Décorateur
Éviter La Répétition De Code (DRY)
Si tu écris dix fonctions qui doivent toutes logger leur exécution, mesurer le temps, ou vérifier des permissions, tu as trois options : copier-coller, ajouter du code en début/fin de chaque fonction… ou utiliser un décorateur. Le décorateur encapsule ce « tour de passe-passe » une fois, et l’applique partout, sans polluer la logique métier.
Exemple typique :
import time
def timeit(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
duration = (time.perf_counter() - start) * 1000
print(f"{func.__name__} a pris {duration:.2f} ms")
return wrapper
@timeit
def calc():
return sum(range(10_000))
Tu gardes ton code métier net, et tu factorises le comportement transversal.
Séparer Les Préoccupations (Cross-Cutting Concerns)
Logging, retries, validation d’arguments, authentification, cache, instrumentation… tout ça coupe transversalement ton code. Les décorateurs t’aident à séparer ces préoccupations de la logique principale, rendant le code plus lisible et plus testable. Tu peux lire la fonction et comprendre « ce qu’elle fait », pendant que le décorateur gère « comment » elle est exécutée.
Comparer Aux Alternatives : Héritage, Mixins, Contexte
- Héritage/mixins: utiles en POO, mais inadaptés pour des fonctions libres et parfois verbeux pour juste ajouter un log.
- Gestionnaires de contexte (with): parfaits pour encadrer un bloc, pas une fonction entière appelée en divers endroits.
- Appels manuels: tu peux appeler helper(func) ici ou là, mais tu oublieras un endroit tôt ou tard. Le décorateur garantit l’application systématique, syntaxe compacte à la clé.
En pratique, les décorateurs Python sont le meilleur compromis lisibilité/portée/simplicité pour ces cas transverses.
Les Bases À Comprendre Avant Les Décorateurs
Les Fonctions Sont Des Objets (Et Valeurs De Première Classe)
En Python, une fonction est un objet: tu peux la passer en argument, la retourner, la stocker dans une variable. Un décorateur n’est donc qu’une fonction qui prend une fonction et renvoie une fonction (ou un objet appelable).
def upper(func):
def wrapper(*args, **kwargs):
return str(func(*args, **kwargs)).upper()
return wrapper
Ici, upper reçoit func et renvoie wrapper. Rien de magique.
Fermetures Et Portée : Capturer Des Variables
Le wrapper « capture » func via une fermeture. Il peut aussi capturer des paramètres du décorateur (on y vient). Comprendre que Python lie les variables libres au moment de la définition t’épargne des surprises, par exemple quand tu boucles pour créer plusieurs wrappers: lie tes variables par défaut si nécessaire.
La Syntaxe @ Et Ce Qu’Elle Fait Réellement
La ligne:
@upper
def hello():
return "salut"
est équivalente à:
def hello():
return "salut"
hello = upper(hello)
Le symbole @ applique la transformation au moment de la définition. Tu peux empiler plusieurs décorateurs: Python les applique de bas en haut (celui juste au-dessus de def est appliqué en premier), et l’attribution se fait de haut en bas.
Écrire Vos Propres Décorateurs
Décorateur Minimal Sans Arguments
Begin simple: accepte la fonction, renvoie un wrapper qui appelle la fonction.
def debug(func):
def wrapper(*args, **kwargs):
print(f"→ {func.__name__} args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"← {func.__name__} -> {result}")
return result
return wrapper
@debug
def add(a, b):
return a + b
Préserver Le Nom Et La Doc Avec functools.wraps
Sans précaution, wrapper masque le nom, la docstring et la signature de la fonction décorée. Pour l’introspection, l’aide, et les outils (Sphinx, IDE, linters), utilise wraps.
from functools import wraps
def debug(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"→ {func.__name__} args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"← {func.__name__} -> {result}")
return result
return wrapper
Avec @wraps, hello.name et help(hello) restent corrects.
Empiler Plusieurs Décorateurs Et Ordre D’Exécution
L’ordre compte. Si tu écris:
@cache
@debug
def f(x):
...
C’est équivalent à f = cache(debug(f)). debug s’exécute donc en premier lors de la définition interne, mais à l’appel c’est le wrapper de cache qui s’exécute et peut décider d’appeler ou non le wrapper de debug selon un hit/miss. Retenir « le décorateur le plus proche de def s’applique d’abord, celui du haut entoure les autres » évite des surprises.
Décorateurs Paramétrés Et Stateful (Compteurs, Caches)
Un décorateur paramétré est une fabrique de décorateurs: une fonction qui retourne un décorateur.
from functools import wraps
def only_roles(*roles): # décorateur paramétré
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if user.role not in roles:
raise PermissionError("Accès refusé")
return func(user, *args, **kwargs)
return wrapper
return decorator
@only_roles("admin", "editor")
def publish(user, post_id):
...
Stateful ? Tu peux garder de l’état dans la fermeture ou dans un objet appelable.
from functools import wraps
def count_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
wrapper.calls += 1
return func(*args, **kwargs)
wrapper.calls = 0
return wrapper
@count_calls
def work():
pass
work.calls te donne le nombre d’appels. Pour des caches, préfère functools.lru_cache qui gère les clés et l’invalidation de base.
Décorateurs Standard Et Cas D’Usage
@property, @staticmethod, @classmethod : Quand Les Utiliser
- @property transforme une méthode en attribut calculé. Tu l’utilises pour exposer une valeur dérivée sans parenthèses, avec possibilité de setter si besoin.
class Rectangle:
def __init__(self, w, h):
self.w, self.h = w, h
@property
def area(self):
return self.w * self.h
- @staticmethod indique une fonction utilitaire attachée à la classe, sans self.
- @classmethod reçoit la classe (cls). Pratique pour des constructeurs alternatifs.
class User:
def __init__(self, name):
self.name = name
@classmethod
def from_email(cls, email):
return cls(email.split("@")[0])
@functools.lru_cache Pour La Mémoïsation
Quand une fonction pure est coûteuse et appelée avec les mêmes arguments, lru_cache garde les résultats.
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
Tu peux invalider avec fib.cache_clear() et inspecter avec fib.cache_info().
@dataclasses.dataclass Et Autres Décorateurs Utiles
@dataclass génère init, repr, eq… pour toi. Idéal pour des objets de données.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
À noter aussi @contextlib.contextmanager pour créer des gestionnaires de contexte à partir de générateurs, ou @functools.singledispatch pour du polymorphisme sur types d’arguments.
Exemples Pratiques : Logging, Timing, Validation, Retry
- Logging: centralise le format et le niveau.
import logging
from functools import wraps
log = logging.getLogger(__name__)
def logged(level=logging.INFO):
def deco(func):
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, "appel %s", func.__name__)
return func(*args, **kwargs)
return wrapper
return deco
- Timing: voir timeit plus haut ou utilise time.perf_counter.
- Validation: vérifie des invariants avant l’appel.
from functools import wraps
def non_negative_args(func):
@wraps(func)
def wrapper(*args, **kwargs):
if any(a < 0 for a in args if isinstance(a, (int, float))):
raise ValueError("Arguments négatifs interdits")
return func(*args, **kwargs)
return wrapper
- Retry: réessaie sur exceptions transitoires.
import time
from functools import wraps
def retry(times=3, delay=0.1, exceptions=(Exception,)):
def deco(func):
@wraps(func)
def wrapper(*args, **kwargs):
last = None
for i in range(times):
try:
return func(*args, **kwargs)
except exceptions as e:
last = e
if i < times - 1:
time.sleep(delay)
raise last
return wrapper
return deco
Pièges, Tests Et Bonnes Pratiques
Signatures Et Typage: wraps, inspect Et functools.update_wrapper
Si tu exposes une API, la signature compte. @wraps copie le nom, la doc et quelques attributs, mais pas la signature dynamique si tu la modifies. Utilise inspect.signature pour relayer proprement, ou pense à typing.ParamSpec et Concatenate (Python 3.10+) pour propager les types dans les décorateurs génériques. Pour les cas avancés, functools.update_wrapper fait la même chose que wraps mais en mode fonction.
Erreurs Courantes: *args/**kwargs, Exceptions Masquées, Ordre
Oublier *args, **kwargs est la chute n°1: ton décorateur cassera dès que la fonction reçoit un argument non prévu. Autre piège: avaler les exceptions sans les repropager, ce qui rend le débogage infernal. Laisse remonter ou re-lève avec contexte. Enfin, vérifie l’ordre des décorateurs empilés: un cache avant un logger ne loguera pas un hit, un retry autour d’un timeout mal placé peut masquer une vraie panne.
Tester Un Décorateur Et Faciliter Le Débogage
Teste le décorateur séparément de la fonction métier. Crée des fonctions jouets et vérifie le comportement attendu: nombre d’appels, propagation des exceptions, respect de la signature. Pour le débogage, conserve les attributs via @wraps, expose l’état (ex: wrapper.calls) et fournis un moyen de désactiver le décorateur via une variable d’environnement ou un flag (utile en prod). Pour diagnostiquer, logge l’entrée/sortie et, si besoin, permets un mode verbose conditionnel.
Conclusion
Tu sais maintenant ce que fait vraiment la syntaxe @, quand choisir un décorateur, et comment en écrire des simples, paramétrés ou stateful sans te prendre les pieds dans le tapis. Les décorateurs Python t’aident à rester DRY, clarifier ton code et brancher du logging, de la validation, du cache ou des retries en une ligne. Begin par un petit décorateur debug avec @wraps, empile-le avec @lru_cache sur une fonction pure, puis ajoute un retry: tu verras à quel point ton code gagne en élégance et en fiabilité. Et si tu hésites, garde en tête cette règle: fais simple, lisible, testé.

No responses yet