Retour au blog

Maîtriser les décorateurs Python

Les décorateurs sont l'un des outils les plus puissants de Python. Apprenez à les comprendre et à créer les vôtres pour écrire du code plus propre et réutilisable.

Maîtriser les décorateurs Python

Les décorateurs sont l'un des outils les plus puissants de Python. Ils permettent de modifier le comportement d'une fonction sans toucher à son code. Dans cet article, vous allez comprendre comment ils fonctionnent et comment créer les vôtres.

Les fonctions sont des objets

Avant de parler de décorateurs, il faut comprendre un concept fondamental en Python : les fonctions sont des objets. Elles peuvent être passées en argument, retournées par d'autres fonctions, et assignées à des variables.

def saluer(nom: str) -> str:
    return f"Bonjour {nom} !"

# Une fonction peut être assignée à une variable
ma_fonction = saluer
print(ma_fonction("Python"))

# Une fonction peut être passée en argument
def executer(func, arg):
    return func(arg)

print(executer(saluer, "World"))

Votre premier décorateur

Un décorateur est simplement une fonction qui prend une fonction en argument et retourne une nouvelle fonction. Voici la structure de base :

from functools import wraps

def mon_decorateur(func):
    @wraps(func)  # Préserve les métadonnées de la fonction originale
    def wrapper(*args, **kwargs):
        print(f"Avant l'appel de {func.__name__}")
        resultat = func(*args, **kwargs)
        print(f"Après l'appel de {func.__name__}")
        return resultat
    return wrapper

@mon_decorateur
def dire_bonjour(nom: str) -> str:
    return f"Bonjour {nom} !"

print(dire_bonjour("Alice"))

Cas pratique : mesurer le temps d'exécution

Un cas d'usage classique des décorateurs est de mesurer le temps d'exécution d'une fonction. C'est particulièrement utile pour le profiling de votre code.

import time
from functools import wraps

def timer(func):
    """Mesure et affiche le temps d'exécution d'une fonction."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} exécutée en {end - start:.4f}s")
        return result
    return wrapper

@timer
def calcul_lent():
    """Simule un calcul qui prend du temps."""
    total = sum(i**2 for i in range(1_000_000))
    return total

resultat = calcul_lent()

Décorateurs avec arguments

Parfois, vous voulez paramétrer votre décorateur. Pour cela, il faut ajouter un niveau d'imbrication supplémentaire.

from functools import wraps

def retry(max_attempts: int = 3, delay: float = 1.0):
    """Réessaie une fonction en cas d'échec."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    print(f"Tentative {attempt}/{max_attempts} échouée: {e}")
                    if attempt < max_attempts:
                        time.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

import random

@retry(max_attempts=3, delay=0.5)
def operation_instable():
    """Une opération qui échoue parfois."""
    if random.random() < 0.7:
        raise ValueError("Oups, ça a raté !")
    return "Succès !"

try:
    print(operation_instable())
except ValueError as e:
    print(f"Échec définitif: {e}")
expertise.sh
user@ohcecours:~$ ./aide.sh --expert

Besoin d'accompagnement ?

Nos experts peuvent vous aider.

user@ohcecours:~$ _

Décorateurs de classe

Les décorateurs ne sont pas limités aux fonctions. Vous pouvez aussi décorer des classes entières, ou utiliser des classes comme décorateurs.

class CountCalls:
    """Décorateur qui compte le nombre d'appels à une fonction."""
    
    def __init__(self, func):
        self.func = func
        self.count = 0
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} appelée {self.count} fois")
        return self.func(*args, **kwargs)

@CountCalls
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# Attention : sans cache, fibonacci est appelée de nombreuses fois !
print(f"\nRésultat: {fibonacci(10)}")
print(f"Nombre total d'appels: {fibonacci.count}")

Bonus : les décorateurs built-in

Python fournit plusieurs décorateurs très utiles dans la bibliothèque standard :

from functools import lru_cache, cached_property
from dataclasses import dataclass

# @lru_cache : met en cache les résultats
@lru_cache(maxsize=128)
def fibonacci_cached(n: int) -> int:
    if n < 2:
        return n
    return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)

print(f"fibonacci(30) = {fibonacci_cached(30)}")
print(f"Cache info: {fibonacci_cached.cache_info()}")
# @property et @cached_property
class Cercle:
    def __init__(self, rayon: float):
        self._rayon = rayon
    
    @property
    def rayon(self) -> float:
        return self._rayon
    
    @rayon.setter
    def rayon(self, value: float):
        if value < 0:
            raise ValueError("Le rayon doit être positif")
        self._rayon = value
    
    @property
    def aire(self) -> float:
        import math
        return math.pi * self._rayon ** 2

c = Cercle(5)
print(f"Rayon: {c.rayon}, Aire: {c.aire:.2f}")

Conclusion

Les décorateurs sont un outil essentiel en Python. Ils permettent de :

  • Séparer les préoccupations : logging, timing, validation, caching...
  • Réutiliser du code : un décorateur peut être appliqué à plusieurs fonctions
  • Garder un code lisible : la logique métier reste claire

N'oubliez pas d'utiliser @wraps pour préserver les métadonnées de vos fonctions décorées !

newsletter.sh
user@ohcecours:~$ ./subscribe.sh --python

Restez informé

Recevez nos derniers articles.

> Pas de spam, promis. Que du Python.