Aller au contenu

DĂ©corateurs Python 🐍 : fonctions amĂ©liorĂ©es

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 !