Aller au contenu
Développement medium

Décorateurs Python : enrichir vos fonctions (@, wraps)

16 min de lecture

logo python

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 !

  • Comprendre pourquoi les fonctions Python sont des objets de première classe
  • Créer un décorateur qui enveloppe une fonction avec wrapper et return
  • 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, @classmethod et @property

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 variable
salutation = dire_bonjour
# Appeler la fonction via la nouvelle variable
salutation()

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 argument
executer_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 fonction
fonction_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 !

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".

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 wrapper

Dans cet exemple, mon_decorateur enveloppe une fonction avec un "emballage" (wrapper) contenant du code supplémentaire.

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 fonction
Bonjour tout le monde !
Après la fonction

Plutôt cool, non ? Mais il y a une façon encore plus simple d’appliquer un décorateur : utiliser la syntaxe @.

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_decorateur
def dire_bonjour():
print("Bonjour tout le monde !")
# Appeler directement la fonction
dire_bonjour()

Cette syntaxe est équivalente à l’exemple précédent, mais bien plus lisible.

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_decorateur
def dire_bonjour_avec_nom(nom):
print(f"Bonjour, {nom} !")
dire_bonjour_avec_nom("Alice")

Sortie :

Avant la fonction
Bonjour, Alice !
Après la fonction

Le *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.

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_decorateur
def 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__.

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
@chronometre
def traitement_lourd():
time.sleep(2) # Simule un calcul lourd
print("Traitement terminé !")
traitement_lourd()

Sortie :

Traitement terminé !
Durée : 2.0021 secondes

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.

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_2
def ma_fonction():
print("Exécution de la fonction")
ma_fonction()

Sortie :

Décorateur 1 : Avant la fonction
Décorateur 2 : Avant la fonction
Exécution de la fonction
Décorateur 2 : Après la fonction
Décorateur 1 : Après la fonction

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 :

  1. ma_fonction est décorée par decorateur_2.
  2. Puis, le résultat est à nouveau décoré par decorateur_1.

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
@chronometre
def 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 secondes
Traitement terminé !

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_donnees
Données supprimées.

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 (cls au lieu de self).

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'instance
print(MathUtils.addition(3, 5)) # Sortie : 8

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 classe
Compteur.incrementer()
Compteur.incrementer()
print(Compteur.total) # Sortie : 2

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
# Utilisation
p = Personne("Alice")
print(p.nom) # Sortie : Alice
p.nom = "Bob"
print(p.nom) # Sortie : Bob

Ici, @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 enrichissent les fonctions, et Click exploite pleinement cette puissance pour simplifier la création de CLI (Command Line Interface). En voici les principaux :

  1. @click.command : transforme une fonction en commande CLI.
  2. @click.option : ajoute des options avec préfixe (--nom).
  3. @click.argument : gère les arguments positionnels.
  4. @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()
  • Un décorateur est une fonction qui prend une fonction, l'enveloppe dans un wrapper et renvoie la version enrichie.
  • La syntaxe @decorateur au-dessus d'une fonction équivaut à func = decorateur(func).
  • Un wrapper en *args, **kwargs rend 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, @classmethod et @property.

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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn