Comprendre et utiliser les décorateurs en Python : guide complet avec exemples

Introduction

Les décorateurs sont l’une des fonctionnalités les plus puissantes — et souvent déroutantes — de Python. À première vue, la syntaxe @decorator peut sembler magique. Mais une fois que l’on comprend comment fonctionnent les décorateurs, ils deviennent un outil élégant pour écrire du code plus propre et plus réutilisable.

Qu’est-ce qu’un décorateur en Python ?

Un décorateur est une fonction qui :

  • Prend une autre fonction en entrée
  • Étend ou modifie son comportement
  • Retourne une nouvelle fonction

Idée clé : les décorateurs permettent d’ajouter des fonctionnalités sans modifier le code de la fonction originale.

En Python, les fonctions sont des objets de première classe, ce qui signifie que :

  • Elles peuvent être passées en argument
  • Elles peuvent être retournées par d’autres fonctions
  • Elles peuvent être assignées à des variables

Les décorateurs reposent entièrement sur ce concept.

Un exemple de fonction simple

Commençons par une fonction basique :

1
2
def greet():
    print("Hello!")

Appel de la fonction :

1
greet()

Sortie :

1
Hello!

Supposons maintenant que nous voulions :

  • Afficher quelque chose avant et après l’exécution de greet()

Nous pourrions modifier la fonction — mais cette approche ne passe pas à l’échelle.

C’est précisément là que les décorateurs deviennent utiles.

Écrire votre premier décorateur

Voici un décorateur simple :

1
2
3
4
5
6
def my_decorator(func):
    def wrapper():
        print("Before the function runs")
        func()
        print("After the function runs")
    return wrapper

Décomposons-le :

  • func est la fonction décorée
  • wrapper() ajoute un comportement supplémentaire
  • func() appelle la fonction originale
  • Le décorateur retourne la nouvelle fonction

Application manuelle :

1
2
3
4
5
def greet():
    print("Hello!")

greet = my_decorator(greet)
greet()

Sortie :

1
2
3
Before the function runs
Hello!
After the function runs

La syntaxe @decorator

Python fournit une syntaxe plus propre avec @ :

1
2
3
@my_decorator
def greet():
    print("Hello!")

Ceci est équivalent à :

1
greet = my_decorator(greet)

Beaucoup plus lisible — et plus élégant.

Décorateurs avec arguments

La plupart des fonctions réelles acceptent des paramètres. Mettons à jour notre décorateur :

1
2
3
4
5
6
7
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before function")
        result = func(*args, **kwargs)
        print("After function")
        return result
    return wrapper

Il fonctionne maintenant avec n’importe quelle fonction :

1
2
3
4
5
@my_decorator
def add(a, b):
    return a + b

print(add(3, 5))

Sortie :

1
2
3
Before function
After function
8

Préserver les métadonnées de la fonction (functools.wraps)

Sans précaution particulière, les décorateurs peuvent écraser :

  • Le nom de la fonction
  • La docstring
  • Les annotations

Exemple du problème :

1
print(add.__name__)  # wrapper

Solution avec functools.wraps :

1
2
3
4
5
6
7
from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Les métadonnées sont maintenant préservées :

1
print(add.__name__)  # add

Décorateurs avec paramètres (avancé)

Parfois, les décorateurs eux-mêmes ont besoin d’arguments :

1
2
3
4
5
6
7
def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

Utilisation :

1
2
3
4
5
@repeat(3)
def say_hi():
    print("Hi!")

say_hi()

Sortie :

1
2
3
Hi!
Hi!
Hi!

Cas d’usage courants dans le monde réel

Journalisation (logging)

1
2
3
4
5
def log(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

Mesure du temps d’exécution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f}s")
        return result
    return wrapper

Authentification / permissions

Les décorateurs sont largement utilisés dans des frameworks web comme Flask et Django.

1
2
3
4
5
6
def require_login(func):
    def wrapper(user):
        if not user.is_authenticated:
            raise PermissionError("Login required")
        return func(user)
    return wrapper

Décorateurs intégrés que vous utilisez déjà

Python inclut de nombreux décorateurs intégrés :

  • @staticmethod
  • @classmethod
  • @property
  • @dataclass
  • @lru_cache

Décorateur @lru_cache en Python

Exemple :

1
2
3
4
5
6
7
from functools import lru_cache

@lru_cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

Décorateur @classmethod en Python

Exemple d’utilisation du décorateur @classmethod en Python

Exemple de base

1
2
3
4
5
6
class MyClass:
    value = 10

    @classmethod
    def show_value(cls):
        return cls.value

Comment ça fonctionne

1
print(MyClass.show_value())   # 10
  • @classmethod reçoit la classe elle-même comme premier argument (cls)
  • Il n’est pas nécessaire de créer une instance
  • Il peut accéder aux variables de classe (cls.value)

Comparaison avec une méthode d’instance

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class MyClass:
    value = 10

    def instance_method(self):
        return self.value

    @classmethod
    def class_method(cls):
        return cls.value

obj = MyClass()

print(obj.instance_method())  # 10
print(MyClass.class_method()) # 10
  • self → instance
  • cls → classe

Exemple pratique : constructeur alternatif

C’est le cas d’usage le plus courant en pratique.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class FirePixel:
    def __init__(self, lat, lon, frp):
        self.lat = lat
        self.lon = lon
        self.frp = frp

    @classmethod
    def from_dict(cls, data):
        return cls(
            lat=data["latitude"],
            lon=data["longitude"],
            frp=data["frp"]
        )

Utilisation

1
2
3
4
5
d = {"latitude": 34.2, "longitude": -118.4, "frp": 56.7}

pixel = FirePixel.from_dict(d)

print(pixel.lat, pixel.lon, pixel.frp)

Pourquoi utiliser @classmethod ici ?

  • Crée une instance sans coder en dur le nom de la classe
  • Fonctionne correctement si la classe est sous-classée

Avantage avec l’héritage (sous-classement)

1
2
3
4
5
class VIIRSPixel(FirePixel):
    pass

p = VIIRSPixel.from_dict(d)
print(type(p))  # <class '__main__.VIIRSPixel'>

Si from_dict était une @staticmethod, cela ne fonctionnerait pas correctement.

Quand utiliser @classmethod

Utilisez-le lorsque :

  • Vous avez besoin d’accéder à la classe, et non à une instance
  • Vous voulez définir des méthodes de fabrique / constructeurs alternatifs
  • Vous souhaitez un code qui respecte l’héritage

Quand ne PAS l’utiliser

  • Si vous avez seulement besoin des données de l’instance → utilisez une méthode normale
  • Si vous n’avez pas besoin de cls → utilisez @staticmethod

Quand faut-il utiliser des décorateurs ?

Utilisez des décorateurs lorsque vous voulez :

  • Ajouter un comportement transversal (logging, timing, cache)
  • Éviter la duplication de code
  • Garder la logique métier claire
  • Appliquer un comportement de manière cohérente à plusieurs fonctions

Évitez les décorateurs lorsque :

  • La logique est très spécifique à une seule fonction
  • La lisibilité du code en souffrirait

Références