Aller au contenu

Comprendre Les Décorateurs Python

Mise à jour :

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 !

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 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 !

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

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.

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

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

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

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

Sortie :

Traitement terminé !
Durée : 2.0021 secondes

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

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

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 :

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

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
@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é !

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

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

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

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

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
# 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 de Click

Dans un autre chapitre, nous avons vu comment les décorateurs enrichissent les fonctions. Click exploite cette puissance pour simplifier la création de CLI (Command Line Interface). En voici la liste :

  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()

Conclusion

Les décorateurs Python sont une solution élégante pour enrichir vos fonctions et simplifier votre code. Qu’il s’agisse de valider des données, de mesurer des performances ou de structurer des CLI avec des outils comme Click, ils offrent une modularité et une lisibilité incomparables.

En maîtrisant les décorateurs, vous pouvez écrire un code plus propre et plus maintenable, tout en réduisant les répétitions inutiles. Les perspectives sont vastes : frameworks, outils d’automatisation ou optimisation de vos propres projets. Lancez-vous, et découvrez à quel point les décorateurs peuvent transformer votre manière de coder !