Skip to main content

Code that compiles on the first try! CodeWithMpia wishes you very happy holidays.✨

Aller au contenu principal

Les Décorateurs en Python : Modifier le Comportement de vos Fonctions

Ce guide pratique démystifie les décorateurs en Python et montre comment ils permettent de modifier le comportement des fonctions (logging, mesure de temps, cache, validation, etc.) sans toucher à leur code, afin d’écrire un code propre, réutilisable et maintenable.

M
Mpia
12/14/2025 33 min
75 vues
0 comme.
#Python#Code#Decorator
Les Décorateurs en Python : Modifier le Comportement de vos Fonctions

Les décorateurs sont l'une des fonctionnalités les plus puissantes et élégantes de Python, mais aussi l'une des plus mystérieuses pour les débutants. Si vous avez déjà vu ce fameux @ au-dessus d'une fonction et vous êtes demandé ce que c'était, cet article est fait pour vous. Nous allons démystifier les décorateurs, comprendre comment ils fonctionnent réellement sous le capot, et voir comment les utiliser pour écrire du code plus propre et plus maintenable.

Qu'est-ce qu'un décorateur ?

La définition simple

Un décorateur est une fonction qui modifie le comportement d'une autre fonction sans changer son code source. C'est comme emballer un cadeau : le cadeau reste le même, mais vous ajoutez une couche autour.

En Python, les décorateurs utilisent le symbole @ et se placent juste au-dessus de la définition de fonction :

@mon_decorateur
def ma_fonction():
    print("Hello!")

Pourquoi c'est utile ?

Imaginez que vous voulez :

  • Mesurer le temps d'exécution de plusieurs fonctions

  • Vérifier les permissions avant d'exécuter une fonction

  • Logger les appels de fonction

  • Mettre en cache les résultats

  • Valider les paramètres d'entrée

Sans décorateurs, vous devriez ajouter le même code dans chaque fonction. Avec les décorateurs, vous l'écrivez une fois et vous l'appliquez partout où vous en avez besoin.

Les fondations : les fonctions sont des objets

Avant de comprendre les décorateurs, il faut comprendre un concept fondamental de Python : les fonctions sont des objets de première classe. Cela signifie qu'on peut :

  1. Assigner une fonction à une variable

  2. Passer une fonction en paramètre

  3. Retourner une fonction depuis une autre fonction

Exemple 1 : Assigner une fonction

def dire_bonjour():
    return "Bonjour!"

# On peut assigner la fonction à une variable
salutation = dire_bonjour

# Et l'appeler via cette variable
print(salutation())  # "Bonjour!"

# Les deux pointent vers la même fonction
print(dire_bonjour)  # <function dire_bonjour at 0x...>
print(salutation)    # <function dire_bonjour at 0x...>

Exemple 2 : Passer une fonction en paramètre

def executer_deux_fois(fonction):
    """Exécute une fonction deux fois."""
    fonction()
    fonction()

def dire_hello():
    print("Hello!")

# On passe la fonction en paramètre
executer_deux_fois(dire_hello)
# Affiche :
# Hello!
# Hello!

Exemple 3 : Retourner une fonction

def creer_salutation(nom):
    """Retourne une fonction de salutation personnalisée."""
    def saluer():
        return f"Bonjour {nom}!"

    return saluer

# On crée deux fonctions différentes
saluer_alice = creer_salutation("Alice")
saluer_bob = creer_salutation("Bob")

print(saluer_alice())  # "Bonjour Alice!"
print(saluer_bob())    # "Bonjour Bob!"

Premier décorateur : comprendre le mécanisme

Créons notre premier décorateur simple qui affiche "Avant" et "Après" autour de l'exécution d'une fonction.

Version manuelle (sans @)

def mon_decorateur(fonction):
    """Enveloppe une fonction avec des messages."""
    def fonction_modifiee():
        print("--- Avant l'exécution ---")
        fonction()
        print("--- Après l'exécution ---")

    return fonction_modifiee

def dire_bonjour():
    print("Bonjour!")

# Application manuelle du décorateur
dire_bonjour = mon_decorateur(dire_bonjour)

# Maintenant dire_bonjour est modifiée
dire_bonjour()

Résultat :

--- Avant l'exécution ---
Bonjour!
--- Après l'exécution ---

Que s'est-il passé ?

  1. mon_decorateur prend une fonction en paramètre

  2. Elle crée une nouvelle fonction fonction_modifiee qui enveloppe l'originale

  3. Elle retourne cette nouvelle fonction

  4. On réassigne dire_bonjour avec cette nouvelle fonction

Version avec @ (syntaxe décorateur)

Python offre une syntaxe plus élégante avec @ :

def mon_decorateur(fonction):
    def fonction_modifiee():
        print("--- Avant l'exécution ---")
        fonction()
        print("--- Après l'exécution ---")
    return fonction_modifiee

@mon_decorateur
def dire_bonjour():
    print("Bonjour!")

# C'est exactement équivalent à :
# dire_bonjour = mon_decorateur(dire_bonjour)

dire_bonjour()

Résultat identique :

--- Avant l'exécution ---
Bonjour!
--- Après l'exécution ---

Le @mon_decorateur fait automatiquement l'assignation pour nous. C'est plus propre et plus lisible !

Décorateurs avec arguments de fonction

Notre premier décorateur ne fonctionne qu'avec des fonctions sans paramètres. Comment gérer les fonctions qui prennent des arguments ?

Le problème

@mon_decorateur
def additionner(a, b):
    return a + b

additionner(3, 5)  # ❌ TypeError: fonction_modifiee() takes 0 positional arguments

La solution : args et *kwargs

def mon_decorateur(fonction):
    def fonction_modifiee(*args, **kwargs):
        print("--- Avant l'exécution ---")
        resultat = fonction(*args, **kwargs)
        print("--- Après l'exécution ---")
        return resultat
    return fonction_modifiee

@mon_decorateur
def additionner(a, b):
    return a + b

@mon_decorateur
def saluer(nom, message="Bonjour"):
    print(f"{message} {nom}!")

# Maintenant ça fonctionne !
print(additionner(3, 5))  # 8
saluer("Alice")
saluer("Bob", message="Salut")

Explication :

  • *args capture tous les arguments positionnels dans un tuple

  • **kwargs capture tous les arguments nommés dans un dictionnaire

  • On les transmet ensuite à la fonction originale avec fonction(*args, **kwargs)

Exemple pratique 1 : Mesurer le temps d'exécution

Un cas d'usage très courant : mesurer combien de temps prend une fonction.

import time

def mesurer_temps(fonction):
    """Mesure et affiche le temps d'exécution d'une fonction."""
    def fonction_chronomee(*args, **kwargs):
        debut = time.time()
        resultat = fonction(*args, **kwargs)
        fin = time.time()
        duree = fin - debut

        print(f"⏱️  {fonction.__name__} a pris {duree:.4f} secondes")
        return resultat

    return fonction_chronomee

@mesurer_temps
def calculer_somme(n):
    """Calcule la somme de 1 à n."""
    total = 0
    for i in range(n + 1):
        total += i
    return total

@mesurer_temps
def calculer_factorielle(n):
    """Calcule la factorielle de n."""
    if n <= 1:
        return 1
    return n * calculer_factorielle(n - 1)

# Test
print(f"Somme : {calculer_somme(1000000)}")
print(f"Factorielle : {calculer_factorielle(10)}")

Résultat :

⏱️  calculer_somme a pris 0.0523 secondes
Somme : 500000500000
⏱️  calculer_factorielle a pris 0.0000 secondes
Factorielle : 3628800

Génial ! On peut maintenant mesurer n'importe quelle fonction juste en ajoutant @mesurer_temps.

Exemple pratique 2 : Logger les appels de fonction

Très utile pour le débogage : enregistrer tous les appels de fonction avec leurs paramètres.

def logger(fonction):
    """Log les appels de fonction avec leurs arguments."""
    def fonction_loggee(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)

        print(f"📝 Appel de {fonction.__name__}({signature})")

        resultat = fonction(*args, **kwargs)

        print(f"✅ {fonction.__name__} a retourné {resultat!r}")
        return resultat

    return fonction_loggee

@logger
def calculer_prix(prix_ht, tva=0.20):
    """Calcule le prix TTC."""
    return prix_ht * (1 + tva)

@logger
def creer_utilisateur(nom, email, age=None):
    """Crée un utilisateur."""
    user = {"nom": nom, "email": email, "age": age}
    return user

# Tests
calculer_prix(100)
calculer_prix(200, tva=0.055)
creer_utilisateur("Alice", "alice@mail.com", age=25)

Résultat :

📝 Appel de calculer_prix(100)
 calculer_prix a retourné 120.0
📝 Appel de calculer_prix(200, tva=0.055)
 calculer_prix a retourné 211.0
📝 Appel de creer_utilisateur('Alice', 'alice@mail.com', age=25)
 creer_utilisateur a retourné {'nom': 'Alice', 'email': 'alice@mail.com', 'age': 25}

Exemple pratique 3 : Mise en cache (memoization)

Optimiser les fonctions coûteuses en mémorisant les résultats déjà calculés.

def memoriser(fonction):
    """Cache les résultats des appels de fonction."""
    cache = {}

    def fonction_mise_en_cache(*args):
        if args in cache:
            print(f"💾 Résultat en cache pour {args}")
            return cache[args]

        print(f"🔄 Calcul pour {args}")
        resultat = fonction(*args)
        cache[args] = resultat
        return resultat

    return fonction_mise_en_cache

@memoriser
def fibonacci(n):
    """Calcule le n-ième nombre de Fibonacci."""
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# Test
print(f"fibonacci(5) = {fibonacci(5)}")
print(f"fibonacci(6) = {fibonacci(6)}")  # Réutilise les calculs précédents
print(f"fibonacci(5) = {fibonacci(5)}")  # Directement depuis le cache

Résultat :

🔄 Calcul pour (0,)
🔄 Calcul pour (1,)
🔄 Calcul pour (2,)
🔄 Calcul pour (3,)
🔄 Calcul pour (4,)
🔄 Calcul pour (5,)
fibonacci(5) = 5
🔄 Calcul pour (6,)
fibonacci(6) = 8
💾 Résultat en cache pour (5,)
fibonacci(5) = 5

Note : Python fournit @functools.lru_cache pour cela, mais c'est instructif de comprendre comment ça fonctionne !

Décorateurs paramétrés : aller plus loin

Parfois, on veut configurer le décorateur lui-même. Par exemple, répéter une fonction N fois où N est configurable.

Le problème

Comment faire pour que @repeter(3) répète la fonction 3 fois ?

La solution : une fonction qui retourne un décorateur

def repeter(nombre_fois):
    """Répète l'exécution d'une fonction."""
    def decorateur(fonction):
        def fonction_repetee(*args, **kwargs):
            for i in range(nombre_fois):
                print(f"--- Exécution {i + 1}/{nombre_fois} ---")
                resultat = fonction(*args, **kwargs)
            return resultat  # Retourne le résultat de la dernière exécution
        return fonction_repetee
    return decorateur

@repeter(3)
def dire_bonjour(nom):
    print(f"Bonjour {nom}!")

dire_bonjour("Alice")

Résultat :

--- Exécution 1/3 ---
Bonjour Alice!
--- Exécution 2/3 ---
Bonjour Alice!
--- Exécution 3/3 ---
Bonjour Alice!

Décortiquons ce qui se passe

@repeter(3)
def dire_bonjour(nom):
    print(f"Bonjour {nom}!")

# Est équivalent à :
# dire_bonjour = repeter(3)(dire_bonjour)

# En détail :
# 1. repeter(3) retourne un décorateur
# 2. Ce décorateur est appliqué à dire_bonjour

C'est un peu comme des poupées russes : une fonction qui retourne une fonction qui retourne une fonction !

Exemple avancé : Vérifier les types d'arguments

Créons un décorateur qui valide les types des arguments passés à une fonction.

def verifier_types(**types_attendus):
    """Vérifie que les arguments ont les bons types."""
    def decorateur(fonction):
        def fonction_verifiee(*args, **kwargs):
            # Récupérer les noms des paramètres
            import inspect
            sig = inspect.signature(fonction)
            params = list(sig.parameters.keys())

            # Vérifier les arguments positionnels
            for i, arg in enumerate(args):
                if i < len(params):
                    param_name = params[i]
                    if param_name in types_attendus:
                        type_attendu = types_attendus[param_name]
                        if not isinstance(arg, type_attendu):
                            raise TypeError(
                                f"{param_name} doit être de type {type_attendu.__name__}, "
                                f"pas {type(arg).__name__}"
                            )

            # Vérifier les arguments nommés
            for param_name, arg in kwargs.items():
                if param_name in types_attendus:
                    type_attendu = types_attendus[param_name]
                    if not isinstance(arg, type_attendu):
                        raise TypeError(
                            f"{param_name} doit être de type {type_attendu.__name__}, "
                            f"pas {type(arg).__name__}"
                        )

            return fonction(*args, **kwargs)

        return fonction_verifiee
    return decorateur

@verifier_types(nom=str, age=int, salaire=float)
def creer_employe(nom, age, salaire):
    return {
        "nom": nom,
        "age": age,
        "salaire": salaire
    }

# Tests
print(creer_employe("Alice", 30, 50000.0))  # ✅ OK
print(creer_employe("Bob", 25, 45000.0))    # ✅ OK

try:
    creer_employe("Charlie", "trente", 55000.0)  # ❌ Erreur
except TypeError as e:
    print(f"❌ Erreur : {e}")

Résultat :

{'nom': 'Alice', 'age': 30, 'salaire': 50000.0}
{'nom': 'Bob', 'age': 25, 'salaire': 45000.0}
 Erreur : age doit être de type int, pas str

Combiner plusieurs décorateurs

On peut appliquer plusieurs décorateurs à une même fonction. Ils s'appliquent de bas en haut.

def gras(fonction):
    def wrapper(*args, **kwargs):
        return f"<b>{fonction(*args, **kwargs)}</b>"
    return wrapper

def italique(fonction):
    def wrapper(*args, **kwargs):
        return f"<i>{fonction(*args, **kwargs)}</i>"
    return wrapper

def souligne(fonction):
    def wrapper(*args, **kwargs):
        return f"<u>{fonction(*args, **kwargs)}</u>"
    return wrapper

@gras
@italique
@souligne
def dire_bonjour(nom):
    return f"Bonjour {nom}"

print(dire_bonjour("Alice"))
# <b><i><u>Bonjour Alice</u></i></b>

Ordre d'application

@decorateur1
@decorateur2
@decorateur3
def ma_fonction():
    pass

# Est équivalent à :
# ma_fonction = decorateur1(decorateur2(decorateur3(ma_fonction)))

Les décorateurs s'appliquent de bas en haut, mais s'exécutent de haut en bas !

Préserver les métadonnées : functools.wraps

Un problème avec nos décorateurs : ils cachent les métadonnées de la fonction originale.

def mon_decorateur(fonction):
    def wrapper(*args, **kwargs):
        return fonction(*args, **kwargs)
    return wrapper

@mon_decorateur
def ma_fonction():
    """Documentation de ma fonction."""
    pass

print(ma_fonction.__name__)  # "wrapper" ❌ Pas "ma_fonction"
print(ma_fonction.__doc__)   # None ❌ Pas la vraie doc

La solution : functools.wraps

from functools import wraps

def mon_decorateur(fonction):
    @wraps(fonction)  # Préserve les métadonnées
    def wrapper(*args, **kwargs):
        return fonction(*args, **kwargs)
    return wrapper

@mon_decorateur
def ma_fonction():
    """Documentation de ma fonction."""
    pass

print(ma_fonction.__name__)  # "ma_fonction" ✅
print(ma_fonction.__doc__)   # "Documentation de ma fonction." ✅

Bonne pratique : Utilisez toujours @wraps(fonction) dans vos décorateurs !

Décorateurs de classe

On peut aussi décorer des classes entières pour modifier leur comportement.

Exemple : Singleton pattern

def singleton(classe):
    """Assure qu'une classe n'a qu'une seule instance."""
    instances = {}

    @wraps(classe)
    def get_instance(*args, **kwargs):
        if classe not in instances:
            instances[classe] = classe(*args, **kwargs)
        return instances[classe]

    return get_instance

@singleton
class Configuration:
    def __init__(self):
        self.parametres = {}
        print("Configuration créée")

    def set(self, cle, valeur):
        self.parametres[cle] = valeur

    def get(self, cle):
        return self.parametres.get(cle)

# Test
config1 = Configuration()  # "Configuration créée"
config1.set("theme", "sombre")

config2 = Configuration()  # Pas de message, c'est la même instance
print(config2.get("theme"))  # "sombre"

print(config1 is config2)  # True ✅

Ajouter des méthodes à une classe

def ajouter_repr(classe):
    """Ajoute une méthode __repr__ à une classe."""
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{classe.__name__}({attrs})"

    classe.__repr__ = __repr__
    return classe

@ajouter_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(3, 4)
print(p)  # Point(x=3, y=4)

Cas d'usage réels : Web frameworks

Les décorateurs sont massivement utilisés dans les frameworks web comme Flask ou Django.

Exemple style Flask

class App:
    def __init__(self):
        self.routes = {}

    def route(self, path):
        """Décorateur pour enregistrer une route."""
        def decorateur(fonction):
            self.routes[path] = fonction
            return fonction
        return decorateur

    def get(self, path):
        """Simule une requête GET."""
        if path in self.routes:
            return self.routes[path]()
        return "404 Not Found"

# Création de l'application
app = App()

@app.route("/")
def accueil():
    return "Bienvenue sur la page d'accueil!"

@app.route("/about")
def about():
    return "À propos de notre site"

@app.route("/contact")
def contact():
    return "Contactez-nous à contact@site.com"

# Simulation de requêtes
print(app.get("/"))       # "Bienvenue sur la page d'accueil!"
print(app.get("/about"))  # "À propos de notre site"
print(app.get("/404"))    # "404 Not Found"

Exercices pratiques

Essayez de créer ces décorateurs par vous-même :

Exercice 1 : Décorateur de validation

Créez un décorateur @valider_positif qui vérifie que tous les arguments numériques sont positifs.

Exercice 2 : Décorateur de retry

Créez un décorateur @retry(max_tentatives=3) qui réessaie une fonction en cas d'exception.

Exercice 3 : Décorateur de rate limiting

Créez un décorateur qui limite le nombre d'appels à une fonction par seconde.

Exercice 4 : Décorateur de dépréciation

Créez un décorateur @deprecated qui affiche un avertissement quand une fonction obsolète est appelée.

Solutions

Cliquez pour voir les solutions
from functools import wraps
import time
import warnings

# Solution 1 : Validation
def valider_positif(fonction):
    @wraps(fonction)
    def wrapper(*args, **kwargs):
        for arg in args:
            if isinstance(arg, (int, float)) and arg < 0:
                raise ValueError(f"Tous les arguments doivent être positifs, {arg} est négatif")
        return fonction(*args, **kwargs)
    return wrapper

@valider_positif
def calculer_racine(nombre):
    return nombre ** 0.5

# Solution 2 : Retry
def retry(max_tentatives=3):
    def decorateur(fonction):
        @wraps(fonction)
        def wrapper(*args, **kwargs):
            for tentative in range(max_tentatives):
                try:
                    return fonction(*args, **kwargs)
                except Exception as e:
                    if tentative == max_tentatives - 1:
                        raise
                    print(f"Tentative {tentative + 1} échouée, réessai...")
            return None
        return wrapper
    return decorateur

# Solution 3 : Rate limiting
def rate_limit(max_par_seconde):
    def decorateur(fonction):
        derniers_appels = []

        @wraps(fonction)
        def wrapper(*args, **kwargs):
            maintenant = time.time()
            # Nettoyer les vieux appels
            derniers_appels[:] = [t for t in derniers_appels if maintenant - t < 1]

            if len(derniers_appels) >= max_par_seconde:
                raise Exception(f"Limite de {max_par_seconde} appels/seconde dépassée")

            derniers_appels.append(maintenant)
            return fonction(*args, **kwargs)

        return wrapper
    return decorateur

# Solution 4 : Deprecated
def deprecated(fonction):
    @wraps(fonction)
    def wrapper(*args, **kwargs):
        warnings.warn(
            f"{fonction.__name__} est obsolète et sera supprimée dans une future version",
            category=DeprecationWarning,
            stacklevel=2
        )
        return fonction(*args, **kwargs)
    return wrapper

@deprecated
def ancienne_fonction():
    return "Cette fonction est obsolète"

Conclusion

Les décorateurs sont un outil puissant qui permet d'écrire du code plus propre, plus réutilisable et plus maintenable. Ils incarnent le principe DRY (Don't Repeat Yourself) en permettant de factoriser des comportements communs.

Les points clés à retenir :

  1. Un décorateur est une fonction qui prend une fonction et retourne une fonction modifiée

  2. La syntaxe @ est du sucre syntaxique pour rendre le code plus lisible

  3. Utilisez *args et kwargs** pour gérer tous types de signatures de fonction

  4. @wraps préserve les métadonnées de la fonction originale

  5. Les décorateurs paramétrés nécessitent un niveau d'imbrication supplémentaire

  6. Plusieurs décorateurs peuvent être combinés, ils s'appliquent de bas en haut

Les décorateurs sont partout dans l'écosystème Python : frameworks web, tests, ORMs, APIs... Les maîtriser vous permettra de mieux comprendre et utiliser ces outils, mais aussi d'écrire votre propre code plus élégant.

Commencez par des décorateurs simples, puis progressez vers des cas plus complexes. Avec la pratique, vous développerez l'intuition de quand et comment utiliser les décorateurs efficacement.

Bon code ! 🎯


Envie d'explorer d'autres concepts Python avancés ? Découvrez plus de tutoriels sur Python dans la catégorie Python !

Commentaires (0)

Laisser un commentaire

Aucun commentaire pour le moment. Soyez le premier à commenter !