Les décorateurs sont l'une des fonctionnalités les plus puissantes et élégantes de Python, mais aussi l'une des plus mystérieuses pour les débutants. Si vous avez déjà vu ce fameux @ au-dessus d'une fonction et vous êtes demandé ce que c'était, cet article est fait pour vous. Nous allons démystifier les décorateurs, comprendre comment ils fonctionnent réellement sous le capot, et voir comment les utiliser pour écrire du code plus propre et plus maintenable.
Qu'est-ce qu'un décorateur ?
La définition simple
Un décorateur est une fonction qui modifie le comportement d'une autre fonction sans changer son code source. C'est comme emballer un cadeau : le cadeau reste le même, mais vous ajoutez une couche autour.
En Python, les décorateurs utilisent le symbole @ et se placent juste au-dessus de la définition de fonction :
@mon_decorateur
def ma_fonction():
print("Hello!")
Pourquoi c'est utile ?
Imaginez que vous voulez :
-
Mesurer le temps d'exécution de plusieurs fonctions
-
Vérifier les permissions avant d'exécuter une fonction
-
Logger les appels de fonction
-
Mettre en cache les résultats
-
Valider les paramètres d'entrée
Sans décorateurs, vous devriez ajouter le même code dans chaque fonction. Avec les décorateurs, vous l'écrivez une fois et vous l'appliquez partout où vous en avez besoin.
Les fondations : les fonctions sont des objets
Avant de comprendre les décorateurs, il faut comprendre un concept fondamental de Python : les fonctions sont des objets de première classe. Cela signifie qu'on peut :
-
Assigner une fonction à une variable
-
Passer une fonction en paramètre
-
Retourner une fonction depuis une autre fonction
Exemple 1 : Assigner une fonction
def dire_bonjour():
return "Bonjour!"
# On peut assigner la fonction à une variable
salutation = dire_bonjour
# Et l'appeler via cette variable
print(salutation()) # "Bonjour!"
# Les deux pointent vers la même fonction
print(dire_bonjour) # <function dire_bonjour at 0x...>
print(salutation) # <function dire_bonjour at 0x...>
Exemple 2 : Passer une fonction en paramètre
def executer_deux_fois(fonction):
"""Exécute une fonction deux fois."""
fonction()
fonction()
def dire_hello():
print("Hello!")
# On passe la fonction en paramètre
executer_deux_fois(dire_hello)
# Affiche :
# Hello!
# Hello!
Exemple 3 : Retourner une fonction
def creer_salutation(nom):
"""Retourne une fonction de salutation personnalisée."""
def saluer():
return f"Bonjour {nom}!"
return saluer
# On crée deux fonctions différentes
saluer_alice = creer_salutation("Alice")
saluer_bob = creer_salutation("Bob")
print(saluer_alice()) # "Bonjour Alice!"
print(saluer_bob()) # "Bonjour Bob!"
Premier décorateur : comprendre le mécanisme
Créons notre premier décorateur simple qui affiche "Avant" et "Après" autour de l'exécution d'une fonction.
Version manuelle (sans @)
def mon_decorateur(fonction):
"""Enveloppe une fonction avec des messages."""
def fonction_modifiee():
print("--- Avant l'exécution ---")
fonction()
print("--- Après l'exécution ---")
return fonction_modifiee
def dire_bonjour():
print("Bonjour!")
# Application manuelle du décorateur
dire_bonjour = mon_decorateur(dire_bonjour)
# Maintenant dire_bonjour est modifiée
dire_bonjour()
Résultat :
--- Avant l'exécution ---
Bonjour!
--- Après l'exécution ---
Que s'est-il passé ?
-
mon_decorateurprend une fonction en paramètre -
Elle crée une nouvelle fonction
fonction_modifieequi enveloppe l'originale -
Elle retourne cette nouvelle fonction
-
On réassigne
dire_bonjouravec cette nouvelle fonction
Version avec @ (syntaxe décorateur)
Python offre une syntaxe plus élégante avec @ :
def mon_decorateur(fonction):
def fonction_modifiee():
print("--- Avant l'exécution ---")
fonction()
print("--- Après l'exécution ---")
return fonction_modifiee
@mon_decorateur
def dire_bonjour():
print("Bonjour!")
# C'est exactement équivalent à :
# dire_bonjour = mon_decorateur(dire_bonjour)
dire_bonjour()
Résultat identique :
--- Avant l'exécution ---
Bonjour!
--- Après l'exécution ---
Le @mon_decorateur fait automatiquement l'assignation pour nous. C'est plus propre et plus lisible !
Décorateurs avec arguments de fonction
Notre premier décorateur ne fonctionne qu'avec des fonctions sans paramètres. Comment gérer les fonctions qui prennent des arguments ?
Le problème
@mon_decorateur
def additionner(a, b):
return a + b
additionner(3, 5) # ❌ TypeError: fonction_modifiee() takes 0 positional arguments
La solution : args et *kwargs
def mon_decorateur(fonction):
def fonction_modifiee(*args, **kwargs):
print("--- Avant l'exécution ---")
resultat = fonction(*args, **kwargs)
print("--- Après l'exécution ---")
return resultat
return fonction_modifiee
@mon_decorateur
def additionner(a, b):
return a + b
@mon_decorateur
def saluer(nom, message="Bonjour"):
print(f"{message} {nom}!")
# Maintenant ça fonctionne !
print(additionner(3, 5)) # 8
saluer("Alice")
saluer("Bob", message="Salut")
Explication :
-
*argscapture tous les arguments positionnels dans un tuple -
**kwargscapture tous les arguments nommés dans un dictionnaire -
On les transmet ensuite à la fonction originale avec
fonction(*args, **kwargs)
Exemple pratique 1 : Mesurer le temps d'exécution
Un cas d'usage très courant : mesurer combien de temps prend une fonction.
import time
def mesurer_temps(fonction):
"""Mesure et affiche le temps d'exécution d'une fonction."""
def fonction_chronomee(*args, **kwargs):
debut = time.time()
resultat = fonction(*args, **kwargs)
fin = time.time()
duree = fin - debut
print(f"⏱️ {fonction.__name__} a pris {duree:.4f} secondes")
return resultat
return fonction_chronomee
@mesurer_temps
def calculer_somme(n):
"""Calcule la somme de 1 à n."""
total = 0
for i in range(n + 1):
total += i
return total
@mesurer_temps
def calculer_factorielle(n):
"""Calcule la factorielle de n."""
if n <= 1:
return 1
return n * calculer_factorielle(n - 1)
# Test
print(f"Somme : {calculer_somme(1000000)}")
print(f"Factorielle : {calculer_factorielle(10)}")
Résultat :
⏱️ calculer_somme a pris 0.0523 secondes
Somme : 500000500000
⏱️ calculer_factorielle a pris 0.0000 secondes
Factorielle : 3628800
Génial ! On peut maintenant mesurer n'importe quelle fonction juste en ajoutant @mesurer_temps.
Exemple pratique 2 : Logger les appels de fonction
Très utile pour le débogage : enregistrer tous les appels de fonction avec leurs paramètres.
def logger(fonction):
"""Log les appels de fonction avec leurs arguments."""
def fonction_loggee(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"📝 Appel de {fonction.__name__}({signature})")
resultat = fonction(*args, **kwargs)
print(f"✅ {fonction.__name__} a retourné {resultat!r}")
return resultat
return fonction_loggee
@logger
def calculer_prix(prix_ht, tva=0.20):
"""Calcule le prix TTC."""
return prix_ht * (1 + tva)
@logger
def creer_utilisateur(nom, email, age=None):
"""Crée un utilisateur."""
user = {"nom": nom, "email": email, "age": age}
return user
# Tests
calculer_prix(100)
calculer_prix(200, tva=0.055)
creer_utilisateur("Alice", "alice@mail.com", age=25)
Résultat :
📝 Appel de calculer_prix(100)
✅ calculer_prix a retourné 120.0
📝 Appel de calculer_prix(200, tva=0.055)
✅ calculer_prix a retourné 211.0
📝 Appel de creer_utilisateur('Alice', 'alice@mail.com', age=25)
✅ creer_utilisateur a retourné {'nom': 'Alice', 'email': 'alice@mail.com', 'age': 25}
Exemple pratique 3 : Mise en cache (memoization)
Optimiser les fonctions coûteuses en mémorisant les résultats déjà calculés.
def memoriser(fonction):
"""Cache les résultats des appels de fonction."""
cache = {}
def fonction_mise_en_cache(*args):
if args in cache:
print(f"💾 Résultat en cache pour {args}")
return cache[args]
print(f"🔄 Calcul pour {args}")
resultat = fonction(*args)
cache[args] = resultat
return resultat
return fonction_mise_en_cache
@memoriser
def fibonacci(n):
"""Calcule le n-ième nombre de Fibonacci."""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Test
print(f"fibonacci(5) = {fibonacci(5)}")
print(f"fibonacci(6) = {fibonacci(6)}") # Réutilise les calculs précédents
print(f"fibonacci(5) = {fibonacci(5)}") # Directement depuis le cache
Résultat :
🔄 Calcul pour (0,)
🔄 Calcul pour (1,)
🔄 Calcul pour (2,)
🔄 Calcul pour (3,)
🔄 Calcul pour (4,)
🔄 Calcul pour (5,)
fibonacci(5) = 5
🔄 Calcul pour (6,)
fibonacci(6) = 8
💾 Résultat en cache pour (5,)
fibonacci(5) = 5
Note : Python fournit @functools.lru_cache pour cela, mais c'est instructif de comprendre comment ça fonctionne !
Décorateurs paramétrés : aller plus loin
Parfois, on veut configurer le décorateur lui-même. Par exemple, répéter une fonction N fois où N est configurable.
Le problème
Comment faire pour que @repeter(3) répète la fonction 3 fois ?
La solution : une fonction qui retourne un décorateur
def repeter(nombre_fois):
"""Répète l'exécution d'une fonction."""
def decorateur(fonction):
def fonction_repetee(*args, **kwargs):
for i in range(nombre_fois):
print(f"--- Exécution {i + 1}/{nombre_fois} ---")
resultat = fonction(*args, **kwargs)
return resultat # Retourne le résultat de la dernière exécution
return fonction_repetee
return decorateur
@repeter(3)
def dire_bonjour(nom):
print(f"Bonjour {nom}!")
dire_bonjour("Alice")
Résultat :
--- Exécution 1/3 ---
Bonjour Alice!
--- Exécution 2/3 ---
Bonjour Alice!
--- Exécution 3/3 ---
Bonjour Alice!
Décortiquons ce qui se passe
@repeter(3)
def dire_bonjour(nom):
print(f"Bonjour {nom}!")
# Est équivalent à :
# dire_bonjour = repeter(3)(dire_bonjour)
# En détail :
# 1. repeter(3) retourne un décorateur
# 2. Ce décorateur est appliqué à dire_bonjour
C'est un peu comme des poupées russes : une fonction qui retourne une fonction qui retourne une fonction !
Exemple avancé : Vérifier les types d'arguments
Créons un décorateur qui valide les types des arguments passés à une fonction.
def verifier_types(**types_attendus):
"""Vérifie que les arguments ont les bons types."""
def decorateur(fonction):
def fonction_verifiee(*args, **kwargs):
# Récupérer les noms des paramètres
import inspect
sig = inspect.signature(fonction)
params = list(sig.parameters.keys())
# Vérifier les arguments positionnels
for i, arg in enumerate(args):
if i < len(params):
param_name = params[i]
if param_name in types_attendus:
type_attendu = types_attendus[param_name]
if not isinstance(arg, type_attendu):
raise TypeError(
f"{param_name} doit être de type {type_attendu.__name__}, "
f"pas {type(arg).__name__}"
)
# Vérifier les arguments nommés
for param_name, arg in kwargs.items():
if param_name in types_attendus:
type_attendu = types_attendus[param_name]
if not isinstance(arg, type_attendu):
raise TypeError(
f"{param_name} doit être de type {type_attendu.__name__}, "
f"pas {type(arg).__name__}"
)
return fonction(*args, **kwargs)
return fonction_verifiee
return decorateur
@verifier_types(nom=str, age=int, salaire=float)
def creer_employe(nom, age, salaire):
return {
"nom": nom,
"age": age,
"salaire": salaire
}
# Tests
print(creer_employe("Alice", 30, 50000.0)) # ✅ OK
print(creer_employe("Bob", 25, 45000.0)) # ✅ OK
try:
creer_employe("Charlie", "trente", 55000.0) # ❌ Erreur
except TypeError as e:
print(f"❌ Erreur : {e}")
Résultat :
{'nom': 'Alice', 'age': 30, 'salaire': 50000.0}
{'nom': 'Bob', 'age': 25, 'salaire': 45000.0}
❌ Erreur : age doit être de type int, pas str
Combiner plusieurs décorateurs
On peut appliquer plusieurs décorateurs à une même fonction. Ils s'appliquent de bas en haut.
def gras(fonction):
def wrapper(*args, **kwargs):
return f"<b>{fonction(*args, **kwargs)}</b>"
return wrapper
def italique(fonction):
def wrapper(*args, **kwargs):
return f"<i>{fonction(*args, **kwargs)}</i>"
return wrapper
def souligne(fonction):
def wrapper(*args, **kwargs):
return f"<u>{fonction(*args, **kwargs)}</u>"
return wrapper
@gras
@italique
@souligne
def dire_bonjour(nom):
return f"Bonjour {nom}"
print(dire_bonjour("Alice"))
# <b><i><u>Bonjour Alice</u></i></b>
Ordre d'application
@decorateur1
@decorateur2
@decorateur3
def ma_fonction():
pass
# Est équivalent à :
# ma_fonction = decorateur1(decorateur2(decorateur3(ma_fonction)))
Les décorateurs s'appliquent de bas en haut, mais s'exécutent de haut en bas !
Préserver les métadonnées : functools.wraps
Un problème avec nos décorateurs : ils cachent les métadonnées de la fonction originale.
def mon_decorateur(fonction):
def wrapper(*args, **kwargs):
return fonction(*args, **kwargs)
return wrapper
@mon_decorateur
def ma_fonction():
"""Documentation de ma fonction."""
pass
print(ma_fonction.__name__) # "wrapper" ❌ Pas "ma_fonction"
print(ma_fonction.__doc__) # None ❌ Pas la vraie doc
La solution : functools.wraps
from functools import wraps
def mon_decorateur(fonction):
@wraps(fonction) # Préserve les métadonnées
def wrapper(*args, **kwargs):
return fonction(*args, **kwargs)
return wrapper
@mon_decorateur
def ma_fonction():
"""Documentation de ma fonction."""
pass
print(ma_fonction.__name__) # "ma_fonction" ✅
print(ma_fonction.__doc__) # "Documentation de ma fonction." ✅
Bonne pratique : Utilisez toujours @wraps(fonction) dans vos décorateurs !
Décorateurs de classe
On peut aussi décorer des classes entières pour modifier leur comportement.
Exemple : Singleton pattern
def singleton(classe):
"""Assure qu'une classe n'a qu'une seule instance."""
instances = {}
@wraps(classe)
def get_instance(*args, **kwargs):
if classe not in instances:
instances[classe] = classe(*args, **kwargs)
return instances[classe]
return get_instance
@singleton
class Configuration:
def __init__(self):
self.parametres = {}
print("Configuration créée")
def set(self, cle, valeur):
self.parametres[cle] = valeur
def get(self, cle):
return self.parametres.get(cle)
# Test
config1 = Configuration() # "Configuration créée"
config1.set("theme", "sombre")
config2 = Configuration() # Pas de message, c'est la même instance
print(config2.get("theme")) # "sombre"
print(config1 is config2) # True ✅
Ajouter des méthodes à une classe
def ajouter_repr(classe):
"""Ajoute une méthode __repr__ à une classe."""
def __repr__(self):
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{classe.__name__}({attrs})"
classe.__repr__ = __repr__
return classe
@ajouter_repr
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(p) # Point(x=3, y=4)
Cas d'usage réels : Web frameworks
Les décorateurs sont massivement utilisés dans les frameworks web comme Flask ou Django.
Exemple style Flask
class App:
def __init__(self):
self.routes = {}
def route(self, path):
"""Décorateur pour enregistrer une route."""
def decorateur(fonction):
self.routes[path] = fonction
return fonction
return decorateur
def get(self, path):
"""Simule une requête GET."""
if path in self.routes:
return self.routes[path]()
return "404 Not Found"
# Création de l'application
app = App()
@app.route("/")
def accueil():
return "Bienvenue sur la page d'accueil!"
@app.route("/about")
def about():
return "À propos de notre site"
@app.route("/contact")
def contact():
return "Contactez-nous à contact@site.com"
# Simulation de requêtes
print(app.get("/")) # "Bienvenue sur la page d'accueil!"
print(app.get("/about")) # "À propos de notre site"
print(app.get("/404")) # "404 Not Found"
Exercices pratiques
Essayez de créer ces décorateurs par vous-même :
Exercice 1 : Décorateur de validation
Créez un décorateur @valider_positif qui vérifie que tous les arguments numériques sont positifs.
Exercice 2 : Décorateur de retry
Créez un décorateur @retry(max_tentatives=3) qui réessaie une fonction en cas d'exception.
Exercice 3 : Décorateur de rate limiting
Créez un décorateur qui limite le nombre d'appels à une fonction par seconde.
Exercice 4 : Décorateur de dépréciation
Créez un décorateur @deprecated qui affiche un avertissement quand une fonction obsolète est appelée.
Solutions
Cliquez pour voir les solutions
from functools import wraps
import time
import warnings
# Solution 1 : Validation
def valider_positif(fonction):
@wraps(fonction)
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError(f"Tous les arguments doivent être positifs, {arg} est négatif")
return fonction(*args, **kwargs)
return wrapper
@valider_positif
def calculer_racine(nombre):
return nombre ** 0.5
# Solution 2 : Retry
def retry(max_tentatives=3):
def decorateur(fonction):
@wraps(fonction)
def wrapper(*args, **kwargs):
for tentative in range(max_tentatives):
try:
return fonction(*args, **kwargs)
except Exception as e:
if tentative == max_tentatives - 1:
raise
print(f"Tentative {tentative + 1} échouée, réessai...")
return None
return wrapper
return decorateur
# Solution 3 : Rate limiting
def rate_limit(max_par_seconde):
def decorateur(fonction):
derniers_appels = []
@wraps(fonction)
def wrapper(*args, **kwargs):
maintenant = time.time()
# Nettoyer les vieux appels
derniers_appels[:] = [t for t in derniers_appels if maintenant - t < 1]
if len(derniers_appels) >= max_par_seconde:
raise Exception(f"Limite de {max_par_seconde} appels/seconde dépassée")
derniers_appels.append(maintenant)
return fonction(*args, **kwargs)
return wrapper
return decorateur
# Solution 4 : Deprecated
def deprecated(fonction):
@wraps(fonction)
def wrapper(*args, **kwargs):
warnings.warn(
f"{fonction.__name__} est obsolète et sera supprimée dans une future version",
category=DeprecationWarning,
stacklevel=2
)
return fonction(*args, **kwargs)
return wrapper
@deprecated
def ancienne_fonction():
return "Cette fonction est obsolète"
Conclusion
Les décorateurs sont un outil puissant qui permet d'écrire du code plus propre, plus réutilisable et plus maintenable. Ils incarnent le principe DRY (Don't Repeat Yourself) en permettant de factoriser des comportements communs.
Les points clés à retenir :
-
Un décorateur est une fonction qui prend une fonction et retourne une fonction modifiée
-
La syntaxe @ est du sucre syntaxique pour rendre le code plus lisible
-
Utilisez *args et kwargs** pour gérer tous types de signatures de fonction
-
@wraps préserve les métadonnées de la fonction originale
-
Les décorateurs paramétrés nécessitent un niveau d'imbrication supplémentaire
-
Plusieurs décorateurs peuvent être combinés, ils s'appliquent de bas en haut
Les décorateurs sont partout dans l'écosystème Python : frameworks web, tests, ORMs, APIs... Les maîtriser vous permettra de mieux comprendre et utiliser ces outils, mais aussi d'écrire votre propre code plus élégant.
Commencez par des décorateurs simples, puis progressez vers des cas plus complexes. Avec la pratique, vous développerez l'intuition de quand et comment utiliser les décorateurs efficacement.
Bon code ! 🎯
Envie d'explorer d'autres concepts Python avancés ? Découvrez plus de tutoriels sur Python dans la catégorie Python !

Laisser un commentaire