
Les décorateurs Python, c'est un peu comme ajouter une couche de vernis à vos fonctions : ils permettent de modifier ou d'enrichir leur comportement, sans toucher à leur code. En gros, ils prennent une fonction, l’enrobent de logique supplémentaire et renvoient une version boostée. Que vous vouliez mesurer le temps d’exécution d’une méthode, vérifier des permissions ou simplement ajouter un peu de magie, les décorateurs sont vos alliés. Et le meilleur ? Une fois que vous les maîtrisez, vous ne pourrez plus vous en passer !
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre pourquoi les fonctions Python sont des objets de première classe
- Créer un décorateur qui enveloppe une fonction avec
wrapperetreturn - Appliquer un décorateur avec la syntaxe
@et gérer les arguments (*args,**kwargs) - Préserver les métadonnées de la fonction décorée avec
functools.wraps - Empiler plusieurs décorateurs et comprendre leur ordre d'exécution
- Utiliser les décorateurs intégrés
@staticmethod,@classmethodet@property
Comment fonctionnent les fonctions en Python
Section intitulée « Comment fonctionnent les fonctions en Python »Pour bien comprendre les décorateurs, il faut d’abord se plonger dans un concept clé de Python : les fonctions sont des objets de première classe. Ça veut dire quoi ? Tout simplement que les fonctions en Python sont comme n'importe quel autre objet : elles peuvent être stockées dans des variables, passées en argument à d’autres fonctions, ou même être retournées comme résultats.
Prenons un exemple simple pour illustrer cela :
def dire_bonjour(): print("Bonjour !")
# Assigner la fonction à une variablesalutation = dire_bonjour
# Appeler la fonction via la nouvelle variablesalutation()Ici, salutation devient un alias de dire_bonjour. Les deux références
pointent vers le même objet fonction.
On peut aussi passer une fonction comme argument à une autre. C’est ici que la magie des décorateurs commence à apparaître.
def executer_fonction(func): print("Je vais exécuter la fonction passée en argument...") func()
# Passer une fonction comme argumentexecuter_fonction(dire_bonjour)Résultat ? La fonction dire_bonjour est exécutée depuis executer_fonction.
Cela ouvre la voie à des opérations comme ajouter de la logique avant ou après
l’exécution d’une fonction.
Une fonction peut aussi en retourner une autre. Ce comportement est crucial pour comprendre comment les décorateurs modifient des fonctions.
def faire_salut(): def message(): print("Salut !") return message
# Appeler faire_salut et récupérer une fonctionfonction_retournee = faire_salut()fonction_retournee()Ici, faire_salut retourne une nouvelle fonction, message, que l’on peut
appeler indépendamment.
Maintenant, combinons ces idées : une fonction peut à la fois recevoir une autre fonction et en retourner une. C’est littéralement ce que fait un décorateur.
def mon_decorateur(func): def wrapper(): print("Avant la fonction") func() print("Après la fonction") return wrapper
def dire_au_revoir(): print("Au revoir !")
# Décorer "à la main"decorated = mon_decorateur(dire_au_revoir)decorated()Ici, mon_decorateur enveloppe dire_au_revoir dans un nouveau comportement.
La fonction retournée, wrapper, contient les actions avant et après.
Comprendre que les fonctions sont des objets de première classe est essentiel pour utiliser les décorateurs. Cela permet de saisir comment ils interceptent et enrichissent des fonctions existantes. Une fois cette idée en tête, les décorateurs deviennent un outil incroyablement puissant pour optimiser votre code !
Créer un décorateur simple
Section intitulée « Créer un décorateur simple »Maintenant que l'on sait que les fonctions en Python peuvent être passées et retournées comme des objets, créons un décorateur basique. Un décorateur est, en gros, une fonction qui prend une autre fonction en entrée, lui ajoute du code avant ou après et retourne une nouvelle fonction "enrichie".
Structure de base d’un décorateur
Section intitulée « Structure de base d’un décorateur »Voici la structure classique d’un décorateur :
def mon_decorateur(func): def wrapper(): # Code avant l'exécution de la fonction originale print("Avant la fonction") func() # Code après l'exécution de la fonction originale print("Après la fonction") return wrapperDans cet exemple, mon_decorateur enveloppe une fonction avec un "emballage"
(wrapper) contenant du code supplémentaire.
Utilisation d’un décorateur
Section intitulée « Utilisation d’un décorateur »Supposons que nous ayons une fonction dire_bonjour que nous voulons enrichir.
Voici comment l’appliquer manuellement :
def dire_bonjour(): print("Bonjour tout le monde !")
# Décorer la fonction "à la main"decorated = mon_decorateur(dire_bonjour)decorated()Cela produit :
Avant la fonctionBonjour tout le monde !Après la fonctionPlutôt cool, non ? Mais il y a une façon encore plus simple d’appliquer un
décorateur : utiliser la syntaxe @.
La syntaxe @ pour simplifier
Section intitulée « La syntaxe @ pour simplifier »Python fournit une syntaxe dédiée pour appliquer un décorateur. Il suffit
d’utiliser le symbole @ avant le nom du décorateur :
@mon_decorateurdef dire_bonjour(): print("Bonjour tout le monde !")
# Appeler directement la fonctiondire_bonjour()Cette syntaxe est équivalente à l’exemple précédent, mais bien plus lisible.
Décorer des fonctions avec des arguments
Section intitulée « Décorer des fonctions avec des arguments »Et si notre fonction avait des arguments ? Pas de problème, il suffit de passer
ces arguments à func dans le wrapper :
def mon_decorateur(func): def wrapper(*args, **kwargs): print("Avant la fonction") result = func(*args, **kwargs) print("Après la fonction") return result return wrapper
@mon_decorateurdef dire_bonjour_avec_nom(nom): print(f"Bonjour, {nom} !")
dire_bonjour_avec_nom("Alice")Sortie :
Avant la fonctionBonjour, Alice !Après la fonctionLe *args et **kwargs permettent de gérer n'importe quel nombre d'arguments
positionnels ou nommés. C’est indispensable pour des fonctions génériques.
Préserver les métadonnées avec functools.wraps
Section intitulée « Préserver les métadonnées avec functools.wraps »Un décorateur remplace votre fonction par wrapper. Sans précaution, la fonction décorée perd son identité : son __name__ et sa docstring deviennent ceux du wrapper. C'est visible dès qu'on affiche func.__name__ dans un log (on lit wrapper au lieu du vrai nom).
Le décorateur @functools.wraps recopie ces métadonnées depuis la fonction originale vers le wrapper :
import functools
def mon_decorateur(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper
@mon_decorateurdef dire_bonjour(): """Affiche un message de bienvenue.""" print("Bonjour !")
print(dire_bonjour.__name__) # dire_bonjour (et non 'wrapper')print(dire_bonjour.__doc__) # Affiche un message de bienvenue.Ajoutez toujours @functools.wraps(func) sur le wrapper : c'est la bonne pratique numéro un des décorateurs, indispensable pour le débogage et les outils qui lisent __name__.
Pourquoi utiliser des décorateurs ?
Section intitulée « Pourquoi utiliser des décorateurs ? »Créer un décorateur permet de réutiliser du code commun, comme ajouter des logs, mesurer le temps d'exécution, ou gérer des permissions. Par exemple, voici un décorateur pour mesurer la durée d’une fonction :
import time
def chronometre(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"Durée : {end - start:.4f} secondes") return result return wrapper
@chronometredef traitement_lourd(): time.sleep(2) # Simule un calcul lourd print("Traitement terminé !")
traitement_lourd()Sortie :
Traitement terminé !Durée : 2.0021 secondesEmpilement de décorateurs
Section intitulée « Empilement de décorateurs »Saviez-vous que vous pouvez appliquer plusieurs décorateurs à une même fonction ? Oui, Python permet ce qu’on appelle l’empilement de décorateurs, et c’est à la fois pratique et puissant. L’idée est simple : chaque décorateur s’applique successivement, dans l’ordre où ils sont définis, pour enrichir votre fonction étape par étape.
Comment empiler des décorateurs
Section intitulée « Comment empiler des décorateurs »Pour empiler des décorateurs, il suffit de les écrire les uns au-dessus des
autres, avec le symbole @. Voici un exemple simple :
def decorateur_1(func): def wrapper(*args, **kwargs): print("Décorateur 1 : Avant la fonction") result = func(*args, **kwargs) print("Décorateur 1 : Après la fonction") return result return wrapper
def decorateur_2(func): def wrapper(*args, **kwargs): print("Décorateur 2 : Avant la fonction") result = func(*args, **kwargs) print("Décorateur 2 : Après la fonction") return result return wrapper
@decorateur_1@decorateur_2def ma_fonction(): print("Exécution de la fonction")
ma_fonction()Sortie :
Décorateur 1 : Avant la fonctionDécorateur 2 : Avant la fonctionExécution de la fonctionDécorateur 2 : Après la fonctionDécorateur 1 : Après la fonctionOrdre d'exécution
Section intitulée « Ordre d'exécution »L’ordre des décorateurs est important. Dans l’exemple ci-dessus, decorateur_2
est appliqué avant decorateur_1 car Python traite les décorateurs du bas
vers le haut.
Autrement dit :
ma_fonctionest décorée pardecorateur_2.- Puis, le résultat est à nouveau décoré par
decorateur_1.
Exemple pratique : logs et chronométrage
Section intitulée « Exemple pratique : logs et chronométrage »Imaginons que vous souhaitiez à la fois ajouter des logs et mesurer le temps d’exécution d’une fonction. Voici comment faire :
import time
def log_execution(func): def wrapper(*args, **kwargs): print(f"Appel de la fonction {func.__name__} avec {args} et {kwargs}") return func(*args, **kwargs) return wrapper
def chronometre(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"Temps d'exécution de {func.__name__} : {end - start:.4f} secondes") return result return wrapper
@log_execution@chronometredef traitement_lourd(n): time.sleep(n) print("Traitement terminé !")
traitement_lourd(2)Sortie :
Appel de la fonction wrapper avec (2,) et {}Temps d'exécution de traitement_lourd : 2.0021 secondesTraitement terminé !Étendre l’empilement aux méthodes de classes
Section intitulée « Étendre l’empilement aux méthodes de classes »Les décorateurs empilés fonctionnent aussi très bien sur des méthodes de classes. Par exemple, vous pouvez combiner des décorateurs pour vérifier les permissions et ajouter des logs :
def verifier_permissions(func): def wrapper(self, *args, **kwargs): if not self.est_admin: print("Accès refusé.") return return func(self, *args, **kwargs) return wrapper
def log_execution(func): def wrapper(*args, **kwargs): print(f"Exécution de {func.__name__}") return func(*args, **kwargs) return wrapper
class Systeme: def __init__(self, est_admin): self.est_admin = est_admin
@verifier_permissions @log_execution def supprimer_donnees(self): print("Données supprimées.")
utilisateur = Systeme(est_admin=False)utilisateur.supprimer_donnees()
admin = Systeme(est_admin=True)admin.supprimer_donnees()Sortie :
Accès refusé.Exécution de supprimer_donneesDonnées supprimées.Décorateurs dans des classes
Section intitulée « Décorateurs dans des classes »Les décorateurs ne sont pas réservés aux fonctions globales. Vous pouvez aussi les appliquer aux méthodes et aux propriétés d’une classe pour ajouter des comportements ou simplifier la gestion de vos données. C’est une fonctionnalité puissante pour structurer un code objet propre et réutilisable.
En Python, il existe des décorateurs intégrés spécifiques pour gérer les méthodes dans une classe :
@staticmethod: indique qu’une méthode n’a pas accès à l’instance ou à la classe elle-même. C’est une fonction "indépendante" dans une classe.@classmethod: transforme une méthode pour qu’elle reçoive la classe comme premier argument au lieu de l’instance (clsau lieu deself).
Exemple avec @staticmethod
Section intitulée « Exemple avec @staticmethod »Une méthode statique est utile pour des fonctions qui ne dépendent pas des attributs de l’instance ou de la classe.
class MathUtils: @staticmethod def addition(a, b): return a + b
# Appel direct sans créer d'instanceprint(MathUtils.addition(3, 5)) # Sortie : 8Exemple avec @classmethod
Section intitulée « Exemple avec @classmethod »Une méthode de classe permet de travailler avec la classe elle-même.
class Compteur: total = 0
@classmethod def incrementer(cls): cls.total += 1
# Utilisation de la méthode de classeCompteur.incrementer()Compteur.incrementer()print(Compteur.total) # Sortie : 2Décorateurs pour les propriétés
Section intitulée « Décorateurs pour les propriétés »Un autre usage fréquent des décorateurs dans les classes est la gestion des
propriétés avec @property. Cela permet de définir des getters et
setters sans appeler explicitement des méthodes.
class Personne: def __init__(self, nom): self._nom = nom
@property def nom(self): return self._nom
@nom.setter def nom(self, nouveau_nom): if not nouveau_nom: raise ValueError("Le nom ne peut pas être vide.") self._nom = nouveau_nom
# Utilisationp = Personne("Alice")print(p.nom) # Sortie : Alicep.nom = "Bob"print(p.nom) # Sortie : BobIci, @property permet d’accéder à l’attribut _nom comme si c’était une
propriété publique tout en gardant le contrôle sur sa validation.
Les décorateurs de Click
Section intitulée « Les décorateurs de Click »Les décorateurs enrichissent les fonctions, et Click exploite pleinement cette puissance pour simplifier la création de CLI (Command Line Interface). En voici les principaux :
@click.command: transforme une fonction en commande CLI.@click.option: ajoute des options avec préfixe (--nom).@click.argument: gère les arguments positionnels.@click.group: regroupe plusieurs commandes sous une même interface.
import click
@click.command()@click.option("--nom", default="utilisateur", help="Le nom à saluer.")def salut(nom): click.echo(f"Bonjour, {nom} !")
if __name__ == "__main__": salut()À retenir
Section intitulée « À retenir »- Un décorateur est une fonction qui prend une fonction, l'enveloppe dans un
wrapperet renvoie la version enrichie. - La syntaxe
@decorateurau-dessus d'une fonction équivaut àfunc = decorateur(func). - Un wrapper en
*args, **kwargsrend le décorateur compatible avec n'importe quelle signature. - Toujours ajouter
@functools.wraps(func)sur le wrapper pour préserver__name__et la docstring. - Les décorateurs empilés s'appliquent du bas vers le haut, mais s'exécutent du haut vers le bas.
- Python fournit des décorateurs intégrés :
@staticmethod,@classmethodet@property.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les réponses courtes ci-dessous couvrent les questions les plus recherchées sur les décorateurs Python. Chaque exemple est autonome et testé sur Python 3.12.
wrapper) pour ajouter du code avant ou après, et renvoie cette version enrichie.def mon_decorateur(func):
def wrapper():
print("Avant")
func()
print("Après")
return wrapper
Il permet de modifier le comportement d'une fonction sans toucher à son code, en la préfixant de @mon_decorateur.func, définit un wrapper à l'intérieur et le renvoie :def mon_decorateur(func):
def wrapper(*args, **kwargs):
print("Avant la fonction")
result = func(*args, **kwargs)
print("Après la fonction")
return result
return wrapper
@mon_decorateur
def saluer(nom):
print(f"Bonjour, {nom} !")
Le *args, **kwargs rend le décorateur compatible avec n'importe quelle signature de fonction.wrapper : son __name__ et sa docstring deviennent ceux du wrapper (les logs affichent wrapper au lieu du vrai nom).@functools.wraps(func) recopie ces métadonnées depuis la fonction originale :import functools
def mon_decorateur(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
À ajouter systématiquement sur le wrapper, pour le débogage et les outils qui lisent __name__.@decorateur_1
@decorateur_2
def ma_fonction():
...
Ici ma_fonction est d'abord décorée par decorateur_2, puis le résultat par decorateur_1. À l'exécution, decorateur_1 (le plus haut) entre en premier, puis decorateur_2, puis la fonction. L'ordre modifie donc le comportement.@staticmethod: la méthode n'a accès ni à l'instance ni à la classe. C'est une fonction rangée dans la classe.@classmethod: la méthode reçoit la classe comme premier argument (clsau lieu deself) et peut lire ou modifier des attributs de classe.
class Compteur:
total = 0
@classmethod
def incrementer(cls):
cls.total += 1
@staticmethod
def aide():
return "Compte les appels"
Les deux s'appellent sans créer d'instance.@property transforme une méthode en attribut calculé, accessible sans parenthèses, comme un getter. Associé à @nom.setter, il contrôle l'écriture (validation) :class Personne:
def __init__(self, nom):
self._nom = nom
@property
def nom(self):
return self._nom
@nom.setter
def nom(self, valeur):
if not valeur:
raise ValueError("Nom vide")
self._nom = valeur
p = Personne("Alice")
p.nom = "Bob" # passe par le setter (validation)
Vous gardez l'encapsulation tout en exposant une interface d'attribut simple.