W3docs

Les décorateurs Python

Apprenez comment fonctionnent les décorateurs Python : écrire les vôtres, préserver les métadonnées avec functools.wraps, empiler les décorateurs et cas d'usage réels.

Un décorateur est une fonction qui enveloppe une autre fonction pour étendre ou modifier son comportement sans en changer le code source. Les décorateurs sont l'une des fonctionnalités les plus puissantes et idiomatiques de Python — ils sont le moteur derrière @staticmethod, @classmethod, @property, @functools.lru_cache, et de nombreux patterns de frameworks web populaires.

Cette page explique comment fonctionnent les décorateurs, comment écrire les vôtres de zéro, comment passer des arguments aux décorateurs, comment les empiler, et dans quel cas chaque pattern est le plus utile.

Comment fonctionnent les décorateurs

Un décorateur est simplement une fonction qui prend une autre fonction comme argument et retourne une nouvelle fonction. Python fournit la syntaxe @ comme raccourci pour en appliquer un :

@shout
def greet(name):
    return f"hello, {name}"

C'est exactement équivalent à :

def greet(name):
    return f"hello, {name}"

greet = shout(greet)

La ligne @shout indique à Python : après avoir défini greet, passez-la immédiatement à shout et reliez le nom greet à ce que shout retourne. À partir de ce moment, chaque appel à greet(...) passe d'abord par la logique de shout.

Écrire votre premier décorateur

Un décorateur définit normalement une fonction wrapper interne qui appelle la fonction originale et ajoute un comportement supplémentaire autour d'elle :

def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@shout
def greet(name):
    return f"hello, {name}"

print(greet("world"))   # HELLO, WORLD
print(greet("python"))  # HELLO, PYTHON

wrapper accepte *args et **kwargs afin de transmettre toute combinaison d'arguments à func sans modification. Cela rend le décorateur compatible avec n'importe quelle fonction quelle que soit sa signature — une bonne habitude dès le départ.

Pourquoi le wrapper doit retourner la fonction interne

shout se termine par return wrapper, et non return wrapper(). C'est intentionnel : shout construit un nouveau callable, sans l'appeler encore. Si vous écriviez accidentellement return wrapper(), le décorateur s'exécuterait immédiatement au moment de la décoration et greet serait lié à la valeur de retour de wrapper — une chaîne — plutôt qu'au callable lui-même.

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

Chaque fonction Python porte des métadonnées : __name__, __doc__, __module__, et davantage. Sans précaution particulière, un décorateur remplace la fonction originale par wrapper, perdant tout cela :

def shout(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

@shout
def greet(name):
    """Say hello to name."""
    return f"hello, {name}"

print(greet.__name__)  # wrapper  — wrong
print(greet.__doc__)   # None     — lost

Corrigez cela en appliquant @functools.wraps(func) au wrapper. Il copie les métadonnées de la fonction originale sur wrapper :

import functools

def shout(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@shout
def greet(name):
    """Say hello to name."""
    return f"hello, {name}"

print(greet("world"))  # HELLO, WORLD
print(greet.__name__)  # greet
print(greet.__doc__)   # Say hello to name.

Utilisez toujours @functools.wraps dans tout décorateur que vous écrivez. Sans cela, les outils de débogage, les générateurs de documentation et les frameworks de test voient un nom de fonction incorrect. La seule exception est lorsque vous souhaitez intentionnellement masquer l'identité originale.

Exemples pratiques de décorateurs

Logger

Journaliser chaque appel à une fonction avec ses arguments et sa valeur de retour :

import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result!r}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(3, 5)
# Calling add with args=(3, 5) kwargs={}
# add returned 8

Timer

Mesurer le temps d'exécution d'une fonction :

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.6f}s")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

total = slow_sum(1_000_000)
print(total)  # slow_sum took 0.01xxs  then  499999500000

time.perf_counter() est le bon choix ici car il offre la résolution la plus élevée disponible pour les mesures de courte durée.

Mémoïsation (Cache)

Mettre en cache la valeur de retour pour chaque ensemble unique d'arguments afin que la fonction ne soit jamais calculée deux fois pour la même entrée :

import functools

def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

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

print(fibonacci(10))  # 55
print(fibonacci(30))  # 832040

Pour le code en production, préférez le @functools.lru_cache ou @functools.cache intégré (Python 3.9+), qui gèrent les cas limites, la sécurité des threads et les limites de taille du cache. La version artisanale ci-dessus est utile pour comprendre le pattern.

Contrôle d'accès

Protéger une fonction pour qu'elle ne puisse s'exécuter que lorsqu'une condition est remplie :

import functools

def require_auth(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_authenticated"):
            raise PermissionError("Authentication required.")
        return func(user, *args, **kwargs)
    return wrapper

@require_auth
def get_dashboard(user):
    return f"Welcome, {user['name']}!"

guest = {"name": "Guest", "is_authenticated": False}
admin = {"name": "Admin", "is_authenticated": True}

try:
    print(get_dashboard(guest))
except PermissionError as e:
    print(e)                      # Authentication required.

print(get_dashboard(admin))       # Welcome, Admin!

Décorateurs avec arguments

Parfois, vous avez besoin de configurer un décorateur au moment de la décoration — par exemple, pour répéter une fonction un nombre variable de fois. Les décorateurs simples ne peuvent pas prendre directement des arguments supplémentaires car Python passe la fonction, non les arguments. La solution est une fabrique de décorateurs : une fonction qui accepte la configuration et retourne un décorateur :

import functools

def repeat(n):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say(message):
    print(message)

say("hello")
# hello
# hello
# hello

En lisant de l'extérieur vers l'intérieur : @repeat(3) appelle d'abord repeat(3), qui retourne decorator. Python applique ensuite decorator à say, qui retourne wrapper. Ainsi say finit par pointer vers wrapper — même pattern qu'avant, avec le niveau supplémentaire juste pour amener n dans la portée.

L'imbrication peut sembler intimidante au début. Un raccourci mental : la fonction la plus externe contient la configuration, la fonction du milieu contient la fonction décorée, et la fonction la plus interne contient l'appel intercepté.

Empiler plusieurs décorateurs

Vous pouvez appliquer plusieurs décorateurs à une seule fonction en empilant des lignes @. Python les applique du bas vers le haut — le décorateur le plus proche du def est appliqué en premier :

import functools

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<b>" + func(*args, **kwargs) + "</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<i>" + func(*args, **kwargs) + "</i>"
    return wrapper

@bold
@italic
def greet(name):
    return f"Hello, {name}"

print(greet("Alice"))  # <b><i>Hello, Alice</i></b>

Équivalent à greet = bold(italic(greet)). italic enveloppe greet en premier, puis bold enveloppe le résultat. La sortie montre que italic s'exécute plus près de la chaîne brute et bold enveloppe l'extérieur.

Décorateurs basés sur des classes

Une classe peut aussi être un décorateur — tout objet doté d'une méthode __call__ est appelable. Les décorateurs basés sur des classes sont utiles lorsque le décorateur lui-même doit maintenir un état entre les appels :

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call #{self.count} to {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()
say_hello()
print(say_hello.count)  # 2

functools.update_wrapper(self, func) fait le même travail que @functools.wraps — il copie les métadonnées de la fonction originale sur l'instance. Après la décoration, say_hello est une instance de CountCalls, donc say_hello.count est un accès à un attribut ordinaire.

Quand choisir une classe plutôt qu'un décorateur de fonction :

  • Vous avez besoin d'un état persistant (count, cache, indicateurs).
  • Le décorateur possède plusieurs méthodes ou une logique auxiliaire.
  • Vous avez besoin que l'objet décoré soit introspectable en tant que type spécifique.

Pièges des décorateurs

Oublier d'appeler la fonction décorée

Une erreur courante au début est de retourner le wrapper mais d'oublier d'appeler func à l'intérieur :

def broken(func):
    def wrapper(*args, **kwargs):
        print("before")
        # forgot to call func!
    return wrapper

La fonction décorée retourne silencieusement None à chaque fois. Assurez-vous toujours que wrapper appelle func(*args, **kwargs) et retourne son résultat.

Décorer au mauvais niveau

Avec les décorateurs paramétrés, oublier l'appel externe est une erreur fréquente :

# Wrong — 'repeat' receives the function, not a count
@repeat      # should be @repeat(3)
def say(msg):
    print(msg)

Cela passe say à repeat là où n est attendu, provoquant une TypeError lors de l'appel de say.

L'ordre des décorateurs est important

Avec des décorateurs empilés, l'ordre change le comportement. @timer puis @log_calls sur la même fonction chronométrera la version déjà journalisée, tandis que l'inverse journalisera la version déjà chronométrée. Réfléchissez à ce que vous souhaitez que chaque couche voie.

Relation avec les fermetures

La fonction wrapper d'un décorateur est une fermeture — elle capture func depuis la portée englobante et la maintient en vie même après que la fonction décorateur externe a retourné. Comprendre les fermetures rend les mécanismes internes des décorateurs évidents : l'objet cellule contenant func est exactement ce qui permet à wrapper d'appeler la fonction originale longtemps après que shout(greet) s'est terminé.

Pour la syntaxe *args et **kwargs utilisée dans les wrappers, consultez le chapitre dédié. Pour les expressions lambda qui s'associent bien aux décorateurs dans les patterns d'ordre supérieur, consultez le chapitre sur les lambda.

Référence rapide

PatternQuand l'utiliser
wrapper de baseAjouter un comportement avant/après une fonction
@functools.wrapsToujours — préserve __name__, __doc__
Fabrique de décorateurs (3 niveaux)Besoin de configurer le décorateur
Décorateurs empilésComposer plusieurs comportements indépendants
Décorateur basé sur une classeBesoin d'un état persistant entre les appels
@functools.lru_cacheMémoïser des fonctions pures (intégré, prêt pour la production)

Pratique

Pratique
What does @functools.wraps(func) do inside a decorator?
What does @functools.wraps(func) do inside a decorator?
Pratique
Given @bold applied above @italic on a function, which decorator is applied first?
Given @bold applied above @italic on a function, which decorator is applied first?
Pratique
What is a decorator factory?
What is a decorator factory?
Was this page helpful?