Les décorateurs sont l’une des fonctionnalités les plus puissantes de Python. Bien maîtrisés, ils éliminent la duplication de code transversal (logging, retry, cache, validation) sans polluer la logique métier.
Rappel : un décorateur simple
import functools
def log_call(func):
@functools.wraps(func) # préserve __name__, __doc__ de la fonction originale
def wrapper(*args, **kwargs):
print(f"Appel de {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} terminé")
return result
return wrapper
@log_call
def process_order(order_id: int):
"""Traite une commande."""
pass
# Sans @functools.wraps, process_order.__name__ serait "wrapper"
print(process_order.__name__) # "process_order" grâce à wraps
Décorateurs avec arguments
Pour passer des paramètres au décorateur, il faut une couche supplémentaire :
def retry(max_attempts: int = 3, delay: float = 1.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_attempts - 1:
import time
print(f"Tentative {attempt + 1}/{max_attempts} échouée : {e}")
time.sleep(delay * (2 ** attempt)) # backoff exponentiel
raise last_exception
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError))
def call_external_api(endpoint: str) -> dict:
response = requests.get(endpoint, timeout=5)
response.raise_for_status()
return response.json()
Cache avec functools.lru_cache et cache custom
from functools import lru_cache
import time
@lru_cache(maxsize=128)
def get_exchange_rate(currency: str) -> float:
"""Appelé une seule fois par devise — résultat mis en cache."""
return fetch_rate_from_api(currency)
# Cache avec TTL (expiration)
def ttl_cache(seconds: int):
def decorator(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
now = time.time()
if args in cache:
result, timestamp = cache[args]
if now - timestamp < seconds:
return result
result = func(*args)
cache[args] = (result, now)
return result
wrapper.cache_clear = lambda: cache.clear()
return wrapper
return decorator
@ttl_cache(seconds=300) # cache 5 minutes
def get_user_permissions(user_id: int) -> list[str]:
return db.fetch_permissions(user_id)
Décorateur de classe : valider les types à l’entrée
import inspect
from typing import get_type_hints
def validate_types(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
hints = get_type_hints(func)
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for param_name, value in bound.arguments.items():
if param_name in hints and not isinstance(value, hints[param_name]):
raise TypeError(
f"Paramètre '{param_name}' : attendu {hints[param_name].__name__}, "
f"reçu {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
@validate_types
def create_user(name: str, age: int, active: bool = True) -> dict:
return {"name": name, "age": age, "active": active}
create_user("Alice", 30) # OK
create_user("Alice", "trente") # TypeError: attendu int, reçu str
Décorateur de classe avec call
class RateLimit:
"""Limite le nombre d'appels par seconde."""
def __init__(self, calls_per_second: int):
self.calls_per_second = calls_per_second
self.calls = []
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# Nettoyer les appels hors de la fenêtre d'1 seconde
self.calls = [t for t in self.calls if now - t < 1.0]
if len(self.calls) >= self.calls_per_second:
raise Exception(f"Rate limit : {self.calls_per_second} appels/s dépassé")
self.calls.append(now)
return func(*args, **kwargs)
return wrapper
@RateLimit(calls_per_second=10)
def send_notification(user_id: int, message: str):
pass
Notre formation Python couvre les décorateurs et les patterns avancés du langage.