Introduction
Les décorateurs permettent de modifier les comportements des fonctions et/ou méthodes sans toucher au code source de ces dernières. Ils permettent de rendre le code plus clair et concis.
En utilisant le symbole @, ils permettent d'enrichir élégamment les fonctions et méthodes existantes.
Pour bien commencer
Les fonctions: des objets de première classe
En Python, comme évoqué dans le glossaire sur les fonctions, elles sont des objets de première classe.
C'est à dire que les fonctions peuvent être passées en argument, affectées à des variables et retournées par d'autres fonctions. Un concept important qui permet la création de décorateurs.
En effet, un décorateur est une fonction qui prend une autre fonction en argument et retourne une version modifiée de cette fonction.
Séparation et modularité
Dans un décorateur, il est intéressant de pouvoir regrouper des fonctionnalités communes, que l'on peut appliquer à différentes fonctions ou méthodes. Ce qui permet d'éviter de dupliquer le code.
De plus, les décorateurs permettent d'isoler le code principal dans la fonction, et les fonctionnalités transversales comme la journalisation et les permissions dans le décorateur.
Syntaxe et Mécanismes
Fonction décoratrice sans le symbole @
Sans utiliser la syntaxe avec le symbole @, un décorateur est une fonction qui accepte une fonction en paramètre et qui retourne une nouvelle fonction. On aura souvent le terme de wrapper.
def fonction_decoratrice(autre_fonction): def wrapper(*args, **kwargs): # Code exécuté avant l'appel de la fonction décorée print("Avant l'exécution de", autre_fonction.__name__) # Appel de la fonction décorée avec ses arguments result = autre_fonction(*args, **kwargs) # Code exécuté après l'appel de la fonction décorée print("Après l'exécution de", autre_fonction.__name__) return result return wrapper def saluer(nom): print(f"Bonjour, {nom}!") # Application manuelle du décorateur saluer = fonction_decoratrice(saluer) # Maintenant, en appelant saluer, on déclenche le code du wrapper saluer("Alice")
-
fonction_decoratriceest notre décorateur. Il prend en argumentautre_fonction, la fonction à décorer. -
wrapperest la fonction interne qui reçoit les arguments positionnels et nommés via*argset**kwargs.
Nous sommes d'accord pour dire que cette manière de faire paraît un peu compliquée...
Fonction décoratrice avec le symbole @
Python permet d'utiliser les décorateurs de manière plus élégante (et plus simple !). Il est donc possible de remplacer :
def saluer(nom): print(f"Bonjour, {nom}!") # Application manuelle du décorateur saluer = fonction_decoratrice(saluer)
par cette syntaxe plus épurée :
@fonction_decoratrice def saluer(nom): print(f"Bonjour, {nom}!")
Conserver les métadonnées avec functools.wraps
A noter que lorsque vous créez un décorateur, un problème subtil apparaît : la fonction décorée perd ses données originales, telles que son nom et sa documentation. En réalité, la fonction originale va être remplacée par le wrapper.
Heureusement, il existe un moyen pour maintenir l'intégrité de la fonction décorée avec le module functools.
Exemple sans functools.wraps :
def fonction_decoratrice(autre_fonction): def wrapper(*args, **kwargs): print("Avant l'exécution de", autre_fonction.__name__) result = autre_fonction(*args, **kwargs) print("Après l'exécution de", autre_fonction.__name__) return result return wrapper @fonction_decoratrice def saluer(nom): """Affiche un message de salutation.""" print(f"Bonjour, {nom}!") print(saluer.__name__) # Affiche 'wrapper' print(saluer.__doc__) # Affiche None
Puis avec functools.wraps :
from functools import wraps def fonction_decoratrice(autre_fonction): @wraps(autre_fonction) def wrapper(*args, **kwargs): print("Avant l'exécution de", autre_fonction.__name__) result = autre_fonction(*args, **kwargs) print("Après l'exécution de", autre_fonction.__name__) return result return wrapper @fonction_decoratrice def saluer(nom): """Affiche un message de salutation.""" print(f"Bonjour, {nom}!") print(saluer.__name__) # Affiche 'saluer' print(saluer.__doc__) # Affiche 'Affiche un message de salutation.'
Les décorateurs avec paramètres
Les décorateurs paramétrés acceptent des arguments qui permettent de configurer leur propre comportement.
import functools def mon_decorateur(param): def decorateur(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"Avant l'exécution, paramètre: {param}") result = func(*args, **kwargs) print(f"Après l'exécution, paramètre: {param}") return result return wrapper return decorateur @mon_decorateur("Test") def ma_fonction(): print("Exécution de ma_fonction") ma_fonction()
La structure :
-
Une fonction externe,
-
Le décorateur (fonction intermédiaire),
-
Le wrapper (fonction interne)
Le décorateur paramétré est donc une fonction qui retourne un décorateur.
Un exemple parlant avec Django :
from django.contrib.auth.decorators import user_passes_test def is_admin(user): """Vérifie si l'utilisateur est administrateur en testant l'attribut is_staff.""" return user.is_staff @user_passes_test(is_admin) def my_view(request): """ Vue accessible uniquement aux utilisateurs pour lesquels is_admin retourne True. Si l'utilisateur ne satisfait pas la condition, il sera redirigé vers la page de connexion par défaut. """ # Traitement de la vue ...
Dans ce cas, le décorateur permet une logique supplémentaire de permission et de login par rapport à la fonction.
Décorer une méthode
Décorer une méthode permet de modifier ou étendre son comportement, tout comme avec les fonctions.
def log_decorator(methode): def wrapper(*args, **kwargs): print(f"Appel de la méthode {methode.__name__}") return methode(*args, **kwargs) return wrapper class Calculatrice: @log_decorator def additionner(self, a, b): return a + b c = Calculatrice() c.additionner(4, 4)
Décorer une classe
Décorer une classe permet de modifier ou étendre la classe entière.
De cette manière on peut :
-
Modifier les méthodes existantes
-
Ajouter des méthodes et des attributs
# Décorateur de classe def ameliorer_classe(cls): # Modification d'une méthode existante methode_originale = cls.afficher_info def nouvelle_afficher_info(self): print("Version améliorée:") methode_originale(self) print(f"Prix avec taxe: {self.prix * 1.2:.2f}€") cls.afficher_info = nouvelle_afficher_info # Ajout d'une nouvelle méthode def appliquer_remise(self, pourcentage): self.prix = self.prix * (1 - pourcentage / 100) print(f"Remise de {pourcentage}% appliquée! Nouveau prix: {self.prix:.2f}€") cls.appliquer_remise = appliquer_remise # Ajout d'un nouvel attribut cls.origine = "France" return cls # Utilisation du décorateur @ameliorer_classe class Produit: def __init__(self, nom, prix): self.nom = nom self.prix = prix def afficher_info(self): print(f"Produit: {self.nom}") print(f"Prix: {self.prix:.2f}€") p = Produit("Livre Python", 29.99) p.afficher_info() print(f"Origine: {p.origine}") p.appliquer_remise(15) p.afficher_info()
Empiler les décorateurs
Il est possible d'appliquer plusieurs décorateurs à une fonction :
def decorateur1(fonction): def wrapper(*args, **kwargs): print("Décorateur 1 - Avant l'exécution") resultat = fonction(*args, **kwargs) print("Décorateur 1 - Après l'exécution") return resultat return wrapper def decorateur2(fonction): def wrapper(*args, **kwargs): print("Décorateur 2 - Avant l'exécution") resultat = fonction(*args, **kwargs) print("Décorateur 2 - Après l'exécution") return resultat return wrapper @decorateur1 @decorateur2 def ma_fonction(texte): print(f"Exécution de la fonction avec: {texte}") return texte.upper() # Test de la fonction décorée resultat = ma_fonction("Bonjour le monde") print(f"Résultat: {resultat}")
En Python, l'empilement des décorateurs se fait selon cet ordre : ils sont traités du bas vers le haut.