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}")
Besoin d'accompagnement ?
Nos experts peuvent vous aider.
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 !
Restez informé
Recevez nos derniers articles.