PythonDev

Python : patterns de décorateurs avancés

30 août 2026 · Sphinx-Digital

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.