Décorateurs#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

Fonctions de première classe et fermetures#

Les décorateurs reposent sur deux propriétés du langage que nous avons introduites dans les chapitres précédents et qu’il convient ici de rappeler avec précision, car leur compréhension est un prérequis absolu.

En Python, les fonctions sont des objets de première classe (first-class objects) : elles peuvent être passées en argument à d’autres fonctions, retournées comme valeur de retour, stockées dans des variables et dans des structures de données. Une fonction est un objet comme les autres, avec ses attributs (__name__, __doc__, __annotations__, __module__…).

Une fermeture (closure) est une fonction interne qui capture des variables de la portée englobante dans laquelle elle a été définie, même après que cette portée ait disparu de la pile d’appels. C’est le mécanisme qui permet à une fonction retournée de « se souvenir » de son contexte de création.

def créer_salutateur(salutation: str):
    """Retourne une fonction qui salue avec la salutation donnée."""
    def saluer(nom: str) -> str:
        # 'salutation' est capturée depuis la portée englobante
        return f"{salutation}, {nom} !"
    return saluer


bonjour = créer_salutateur("Bonjour")
bonsoir = créer_salutateur("Bonsoir")

print(bonjour("Alice"))
print(bonsoir("Bob"))

# La cellule de fermeture est accessible
print(bonjour.__closure__[0].cell_contents)   # "Bonjour"
Bonjour, Alice !
Bonsoir, Bob !
Bonjour

Décorateur simple#

Un décorateur est une fonction qui prend une fonction en entrée, en retourne une nouvelle (généralement en enveloppant l’originale dans une fermeture), et est appliquée via la syntaxe @nom_du_décorateur placée immédiatement avant la définition de la fonction cible.

Définition 24 (Décorateur)

Un décorateur est un callable qui prend un callable en argument et retourne un callable. La syntaxe @décorateur appliquée à une définition de fonction def f(...) est strictement équivalente à f = décorateur(f). Un décorateur peut modifier le comportement de la fonction enveloppée, ajouter de la logique avant ou après son exécution, ou l’instrumenter sans modifier son code source.

def journaliser(fonction):
    """Décorateur qui journalise chaque appel à la fonction."""
    def enveloppe(*args, **kwargs):
        print(f"Appel de {fonction.__name__!r} avec args={args}, kwargs={kwargs}")
        résultat = fonction(*args, **kwargs)
        print(f"{fonction.__name__!r} a retourné {résultat!r}")
        return résultat
    return enveloppe


@journaliser
def additionner(a: float, b: float) -> float:
    """Additionne deux nombres."""
    return a + b


@journaliser
def saluer(nom: str) -> str:
    return f"Bonjour, {nom} !"


additionner(3, 4)
saluer("Alice")
Appel de 'additionner' avec args=(3, 4), kwargs={}
'additionner' a retourné 7
Appel de 'saluer' avec args=('Alice',), kwargs={}
'saluer' a retourné 'Bonjour, Alice !'
'Bonjour, Alice !'

Appliquons la syntaxe longue pour bien voir ce qui se passe :

def multiplier(a: float, b: float) -> float:
    return a * b

# Ces deux formes sont strictement équivalentes :
# @journaliser              ←→    multiplier = journaliser(multiplier)
multiplier = journaliser(multiplier)
multiplier(5, 6)
Appel de 'multiplier' avec args=(5, 6), kwargs={}
'multiplier' a retourné 30
30

functools.wraps#

La décoration naïve présentée ci-dessus a un défaut : elle remplace la fonction originale par enveloppe, ce qui fait que les métadonnées de la fonction (son nom, sa docstring, ses annotations) sont perdues.

def journaliser_naïf(fonction):
    def enveloppe(*args, **kwargs):
        return fonction(*args, **kwargs)
    return enveloppe

@journaliser_naïf
def ma_fonction():
    """Une docstring importante."""
    pass

print(ma_fonction.__name__)   # "enveloppe" — pas "ma_fonction" !
print(ma_fonction.__doc__)    # None — la docstring est perdue !
enveloppe
None
from functools import wraps

def journaliser_correct(fonction):
    """Décorateur qui préserve les métadonnées grâce à @wraps."""
    @wraps(fonction)    # Copie __name__, __doc__, __annotations__, __module__…
    def enveloppe(*args, **kwargs):
        print(f"Appel de {fonction.__name__!r}")
        return fonction(*args, **kwargs)
    return enveloppe


@journaliser_correct
def ma_fonction():
    """Une docstring importante."""
    pass


print(ma_fonction.__name__)   # "ma_fonction" — correct !
print(ma_fonction.__doc__)    # "Une docstring importante." — préservée !
print(ma_fonction.__wrapped__)  # La fonction originale, accessible via __wrapped__
ma_fonction
Une docstring importante.
<function ma_fonction at 0x7fd474f93e20>

Remarque 23

functools.wraps doit être appliqué systématiquement dans tout décorateur bien écrit. Il copie les attributs __name__, __qualname__, __doc__, __dict__, __module__, __annotations__ et __wrapped__ depuis la fonction originale vers la fonction enveloppe. L’attribut __wrapped__ est particulièrement utile : il permet d’accéder directement à la fonction originale non décorée, ce qui est indispensable pour les tests unitaires.

Décorateur avec arguments#

Un décorateur simple prend une fonction et retourne une fonction. Un décorateur paramétré prend des arguments et retourne un décorateur. On obtient alors une fabrique de décorateurs : une fonction qui, une fois appelée avec les arguments souhaités, retourne un décorateur.

Définition 25 (Décorateur paramétré)

Un décorateur paramétré est une fonction à trois niveaux d’imbrication : le premier niveau reçoit les arguments du décorateur, le second reçoit la fonction à décorer, et le troisième est la fonction enveloppe. La syntaxe @décorateur(args) est équivalente à f = décorateur(args)(f).

from functools import wraps

def répéter(n: int):
    """Fabrique un décorateur qui exécute la fonction n fois."""
    def décorateur(fonction):
        @wraps(fonction)
        def enveloppe(*args, **kwargs):
            résultat = None
            for _ in range(n):
                résultat = fonction(*args, **kwargs)
            return résultat
        return enveloppe
    return décorateur


@répéter(3)
def dire_bonjour(nom: str) -> str:
    print(f"Bonjour, {nom} !")
    return f"Bonjour, {nom} !"


dire_bonjour("Alice")
Bonjour, Alice !
Bonjour, Alice !
Bonjour, Alice !
'Bonjour, Alice !'
def valider_types(**types_attendus):
    """Décorateur paramétré qui valide les types des arguments par nom."""
    def décorateur(fonction):
        @wraps(fonction)
        def enveloppe(*args, **kwargs):
            import inspect
            sig = inspect.signature(fonction)
            paramètres = list(sig.parameters.keys())
            # Vérifier les arguments positionnels
            for i, (param, valeur) in enumerate(zip(paramètres, args)):
                if param in types_attendus:
                    if not isinstance(valeur, types_attendus[param]):
                        raise TypeError(
                            f"Paramètre '{param}' : attendu {types_attendus[param].__name__}, "
                            f"reçu {type(valeur).__name__}"
                        )
            # Vérifier les arguments par mot-clé
            for param, valeur in kwargs.items():
                if param in types_attendus:
                    if not isinstance(valeur, types_attendus[param]):
                        raise TypeError(
                            f"Paramètre '{param}' : attendu {types_attendus[param].__name__}, "
                            f"reçu {type(valeur).__name__}"
                        )
            return fonction(*args, **kwargs)
        return enveloppe
    return décorateur


@valider_types(nom=str, âge=int)
def créer_profil(nom: str, âge: int) -> dict:
    return {"nom": nom, "âge": âge}


print(créer_profil("Alice", 30))

try:
    créer_profil("Bob", "trente")   # âge doit être un int
except TypeError as e:
    print(f"TypeError : {e}")
{'nom': 'Alice', 'âge': 30}
TypeError : Paramètre 'âge' : attendu int, reçu str

Cas d’usage classiques#

Cache et mémoïsation#

Le cache de résultats (mémoïsation) est l’un des cas d’usage les plus emblématiques des décorateurs. La bibliothèque standard fournit functools.lru_cache (et son alias functools.cache depuis Python 3.9).

from functools import lru_cache, cache
import time

@cache   # Cache illimité (équivalent à lru_cache(maxsize=None))
def fibonacci(n: int) -> int:
    """Fibonacci récursif, optimisé par mémoïsation."""
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


t0 = time.perf_counter()
print(fibonacci(40))
t1 = time.perf_counter()
print(f"Durée (premier appel)  : {(t1 - t0) * 1000:.3f} ms")

t0 = time.perf_counter()
print(fibonacci(40))   # Depuis le cache
t1 = time.perf_counter()
print(f"Durée (appel en cache) : {(t1 - t0) * 1000:.3f} ms")

print(f"Informations du cache : {fibonacci.cache_info()}")
102334155
Durée (premier appel)  : 0.124 ms
102334155
Durée (appel en cache) : 0.059 ms
Informations du cache : CacheInfo(hits=39, misses=41, maxsize=None, currsize=41)

Minuterie (timer)#

import time
from functools import wraps

def minuterie(fonction):
    """Mesure et affiche le temps d'exécution d'une fonction."""
    @wraps(fonction)
    def enveloppe(*args, **kwargs):
        début = time.perf_counter()
        résultat = fonction(*args, **kwargs)
        durée = time.perf_counter() - début
        print(f"{fonction.__name__!r} exécutée en {durée * 1000:.3f} ms")
        return résultat
    return enveloppe


@minuterie
def tri_lent(n: int) -> list:
    """Trie une liste de n nombres aléatoires."""
    import random
    données = [random.random() for _ in range(n)]
    return sorted(données)


résultat = tri_lent(100_000)
print(f"Résultat : liste de {len(résultat)} éléments")
'tri_lent' exécutée en 24.626 ms
Résultat : liste de 100000 éléments

Réessai (retry)#

import time
import random
from functools import wraps

def réessayer(tentatives: int = 3, délai: float = 0.1, exceptions=(Exception,)):
    """Réessaie la fonction en cas d'échec, jusqu'à 'tentatives' fois."""
    def décorateur(fonction):
        @wraps(fonction)
        def enveloppe(*args, **kwargs):
            dernière_exception = None
            for i in range(1, tentatives + 1):
                try:
                    return fonction(*args, **kwargs)
                except exceptions as e:
                    dernière_exception = e
                    print(f"Tentative {i}/{tentatives} échouée : {e}")
                    if i < tentatives:
                        time.sleep(délai)
            raise dernière_exception
        return enveloppe
    return décorateur


compteur_appels = 0

@réessayer(tentatives=4, délai=0.01, exceptions=(ValueError,))
def service_instable() -> str:
    """Simule un service qui échoue les premières fois."""
    global compteur_appels
    compteur_appels += 1
    if compteur_appels < 3:
        raise ValueError(f"Échec temporaire (appel #{compteur_appels})")
    return f"Succès à l'appel #{compteur_appels}"


print(service_instable())
Tentative 1/4 échouée : Échec temporaire (appel #1)
Tentative 2/4 échouée : Échec temporaire (appel #2)
Succès à l'appel #3

Empilement de décorateurs#

Plusieurs décorateurs peuvent être empilés sur une même fonction. L’ordre d’application suit la règle de bas en haut : le décorateur le plus proche de def est appliqué en premier.

from functools import wraps

def décorateur_A(fn):
    @wraps(fn)
    def enveloppe(*args, **kwargs):
        print("A — avant")
        résultat = fn(*args, **kwargs)
        print("A — après")
        return résultat
    return enveloppe

def décorateur_B(fn):
    @wraps(fn)
    def enveloppe(*args, **kwargs):
        print("B — avant")
        résultat = fn(*args, **kwargs)
        print("B — après")
        return résultat
    return enveloppe


@décorateur_A
@décorateur_B
def ma_fonction():
    print("Corps de la fonction")


# Équivalent à : ma_fonction = décorateur_A(décorateur_B(ma_fonction))
ma_fonction()
A — avant
B — avant
Corps de la fonction
B — après
A — après
# Exemple pratique : cache + minuterie
@minuterie
@lru_cache(maxsize=256)
def suite_collatz(n: int) -> int:
    """Longueur de la suite de Collatz pour n."""
    if n == 1:
        return 1
    if n % 2 == 0:
        return 1 + suite_collatz(n // 2)
    return 1 + suite_collatz(3 * n + 1)


print(suite_collatz(27))    # Premier appel : calcule
print(suite_collatz(27))    # Deuxième appel : depuis le cache
'suite_collatz' exécutée en 0.002 ms
'suite_collatz' exécutée en 0.104 ms
'suite_collatz' exécutée en 0.144 ms
'suite_collatz' exécutée en 0.168 ms
'suite_collatz' exécutée en 0.191 ms
'suite_collatz' exécutée en 0.212 ms
'suite_collatz' exécutée en 0.279 ms
'suite_collatz' exécutée en 0.361 ms
'suite_collatz' exécutée en 0.471 ms
'suite_collatz' exécutée en 0.548 ms
'suite_collatz' exécutée en 0.577 ms
'suite_collatz' exécutée en 0.597 ms
'suite_collatz' exécutée en 0.617 ms
'suite_collatz' exécutée en 0.637 ms
'suite_collatz' exécutée en 0.656 ms
'suite_collatz' exécutée en 0.677 ms
'suite_collatz' exécutée en 0.696 ms
'suite_collatz' exécutée en 0.720 ms
'suite_collatz' exécutée en 0.739 ms
'suite_collatz' exécutée en 0.759 ms
'suite_collatz' exécutée en 0.782 ms
'suite_collatz' exécutée en 0.802 ms
'suite_collatz' exécutée en 0.823 ms
'suite_collatz' exécutée en 0.842 ms
'suite_collatz' exécutée en 0.862 ms
'suite_collatz' exécutée en 0.882 ms
'suite_collatz' exécutée en 0.901 ms
'suite_collatz' exécutée en 0.921 ms
'suite_collatz' exécutée en 0.939 ms
'suite_collatz' exécutée en 0.958 ms
'suite_collatz' exécutée en 0.978 ms
'suite_collatz' exécutée en 0.996 ms
'suite_collatz' exécutée en 1.014 ms
'suite_collatz' exécutée en 1.033 ms
'suite_collatz' exécutée en 1.052 ms
'suite_collatz' exécutée en 1.076 ms
'suite_collatz' exécutée en 1.099 ms
'suite_collatz' exécutée en 1.117 ms
'suite_collatz' exécutée en 1.135 ms
'suite_collatz' exécutée en 1.154 ms
'suite_collatz' exécutée en 1.173 ms
'suite_collatz' exécutée en 1.192 ms
'suite_collatz' exécutée en 1.213 ms
'suite_collatz' exécutée en 1.232 ms
'suite_collatz' exécutée en 1.250 ms
'suite_collatz' exécutée en 1.268 ms
'suite_collatz' exécutée en 1.286 ms
'suite_collatz' exécutée en 1.305 ms
'suite_collatz' exécutée en 1.331 ms
'suite_collatz' exécutée en 1.354 ms
'suite_collatz' exécutée en 1.372 ms
'suite_collatz' exécutée en 1.390 ms
'suite_collatz' exécutée en 1.409 ms
'suite_collatz' exécutée en 1.435 ms
'suite_collatz' exécutée en 1.453 ms
'suite_collatz' exécutée en 1.472 ms
'suite_collatz' exécutée en 1.490 ms
'suite_collatz' exécutée en 1.508 ms
'suite_collatz' exécutée en 1.526 ms
'suite_collatz' exécutée en 1.544 ms
'suite_collatz' exécutée en 1.563 ms
'suite_collatz' exécutée en 1.581 ms
'suite_collatz' exécutée en 1.599 ms
'suite_collatz' exécutée en 1.663 ms
'suite_collatz' exécutée en 1.734 ms
'suite_collatz' exécutée en 1.949 ms
'suite_collatz' exécutée en 1.973 ms
'suite_collatz' exécutée en 1.992 ms
'suite_collatz' exécutée en 2.022 ms
'suite_collatz' exécutée en 2.041 ms
'suite_collatz' exécutée en 2.059 ms
'suite_collatz' exécutée en 2.078 ms
'suite_collatz' exécutée en 2.096 ms
'suite_collatz' exécutée en 2.114 ms
'suite_collatz' exécutée en 2.132 ms
'suite_collatz' exécutée en 2.152 ms
'suite_collatz' exécutée en 2.170 ms
'suite_collatz' exécutée en 2.189 ms
'suite_collatz' exécutée en 2.207 ms
'suite_collatz' exécutée en 2.225 ms
'suite_collatz' exécutée en 2.243 ms
'suite_collatz' exécutée en 2.260 ms
'suite_collatz' exécutée en 2.278 ms
'suite_collatz' exécutée en 2.297 ms
'suite_collatz' exécutée en 2.314 ms
'suite_collatz' exécutée en 2.335 ms
'suite_collatz' exécutée en 2.376 ms
'suite_collatz' exécutée en 2.396 ms
'suite_collatz' exécutée en 2.415 ms
'suite_collatz' exécutée en 2.434 ms
'suite_collatz' exécutée en 2.452 ms
'suite_collatz' exécutée en 2.471 ms
'suite_collatz' exécutée en 2.489 ms
'suite_collatz' exécutée en 2.507 ms
'suite_collatz' exécutée en 2.526 ms
'suite_collatz' exécutée en 2.544 ms
'suite_collatz' exécutée en 2.563 ms
'suite_collatz' exécutée en 2.581 ms
'suite_collatz' exécutée en 2.599 ms
'suite_collatz' exécutée en 2.617 ms
'suite_collatz' exécutée en 2.636 ms
'suite_collatz' exécutée en 2.654 ms
'suite_collatz' exécutée en 2.672 ms
'suite_collatz' exécutée en 2.690 ms
'suite_collatz' exécutée en 2.708 ms
'suite_collatz' exécutée en 2.726 ms
'suite_collatz' exécutée en 2.745 ms
'suite_collatz' exécutée en 2.763 ms
'suite_collatz' exécutée en 2.782 ms
'suite_collatz' exécutée en 2.801 ms
'suite_collatz' exécutée en 2.820 ms
'suite_collatz' exécutée en 2.840 ms
112
'suite_collatz' exécutée en 0.001 ms
112

Décorateurs de classe#

Un décorateur peut aussi s’appliquer à une classe entière. Il reçoit la classe comme argument et retourne une classe modifiée (ou une nouvelle classe). Ce mécanisme est notamment utilisé par @dataclass de la bibliothèque standard pour générer automatiquement __init__, __repr__ et __eq__.

def singleton(classe):
    """Décorateur de classe garantissant qu'une seule instance est créée."""
    instances: dict = {}

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

    return obtenir_instance


@singleton
class Configuration:
    """Configuration globale de l'application."""

    def __init__(self) -> None:
        self.debug = False
        self.version = "1.0.0"
        print("Configuration créée")

    def __repr__(self) -> str:
        return f"Configuration(debug={self.debug}, version={self.version!r})"


cfg1 = Configuration()
cfg2 = Configuration()   # Ne crée pas de nouvelle instance

print(cfg1 is cfg2)   # True — même objet
cfg1.debug = True
print(cfg2.debug)     # True — le même objet
Configuration créée
True
True
def enregistrer(registre: dict):
    """Décorateur de classe qui enregistre la classe dans un registre."""
    def décorateur(classe):
        registre[classe.__name__] = classe
        return classe
    return décorateur


PLUGINS: dict = {}

@enregistrer(PLUGINS)
class PluginA:
    def exécuter(self): return "Plugin A exécuté"

@enregistrer(PLUGINS)
class PluginB:
    def exécuter(self): return "Plugin B exécuté"

print(f"Plugins enregistrés : {list(PLUGINS.keys())}")
for nom, cls in PLUGINS.items():
    print(f"  {nom}{cls().exécuter()}")
Plugins enregistrés : ['PluginA', 'PluginB']
  PluginA → Plugin A exécuté
  PluginB → Plugin B exécuté

Visualisation du mécanisme de décoration#

Hide code cell source

fig, axes = plt.subplots(1, 2, figsize=(14, 7))
palette = sns.color_palette("Set2", 5)

# ─── Diagramme 1 : mécanisme d'un décorateur simple ───
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis('off')
ax.set_title("Décorateur simple", fontsize=13, fontweight='bold')

blocs = [
    (1.0, 7.5, 8.0, 1.5, palette[0], "def décorateur(fonction):"),
    (1.5, 5.5, 7.0, 1.5, palette[1], "def enveloppe(*args, **kwargs):"),
    (2.0, 3.8, 6.0, 1.2, palette[2], "# logique avant"),
    (2.0, 2.5, 6.0, 1.2, palette[3], "résultat = fonction(*args, **kwargs)"),
    (2.0, 1.2, 6.0, 1.2, palette[4], "# logique après / return résultat"),
]

for (x, y, w, h, col, txt) in blocs:
    box = patches.FancyBboxPatch((x, y), w, h,
        boxstyle="round,pad=0.1", linewidth=1.5,
        edgecolor=col, facecolor=col, alpha=0.2)
    ax.add_patch(box)
    brd = patches.FancyBboxPatch((x, y), w, h,
        boxstyle="round,pad=0.1", linewidth=1.5,
        edgecolor=col, facecolor='none')
    ax.add_patch(brd)
    ax.text(x + w / 2, y + h / 2, txt,
            ha='center', va='center', fontsize=9,
            fontfamily='monospace', color='#222')

ax.text(5, 9.3, "@décorateur", ha='center', fontsize=11,
        fontweight='bold', color=palette[0],
        bbox=dict(boxstyle='round,pad=0.3', facecolor=palette[0], alpha=0.15))
ax.annotate('', xy=(5, 9.0), xytext=(5, 8.8),
            arrowprops=dict(arrowstyle='->', color='#555', lw=1.5))

# ─── Diagramme 2 : ordre d'empilement ───
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 10)
ax2.axis('off')
ax2.set_title("Empilement de décorateurs (ordre d'application)", fontsize=13, fontweight='bold')

couches = [
    (0.5, 7.5, 9.0, 1.5, palette[0], "@décorateur_A", "1ᵉʳ appliqué (externe)"),
    (1.2, 5.5, 7.6, 1.5, palette[1], "@décorateur_B", "2ᵉ appliqué (interne)"),
    (2.2, 3.5, 5.6, 1.5, palette[2], "def f(...):", "Fonction originale"),
]

for (x, y, w, h, col, titre, note) in couches:
    box = patches.FancyBboxPatch((x, y), w, h,
        boxstyle="round,pad=0.12", linewidth=1.8,
        edgecolor=col, facecolor=col, alpha=0.18)
    ax2.add_patch(box)
    brd = patches.FancyBboxPatch((x, y), w, h,
        boxstyle="round,pad=0.12", linewidth=1.8,
        edgecolor=col, facecolor='none')
    ax2.add_patch(brd)
    ax2.text(x + w / 2, y + h - 0.45, titre,
             ha='center', va='center', fontsize=11,
             fontfamily='monospace', fontweight='bold', color=col)
    ax2.text(x + w / 2, y + 0.4, note,
             ha='center', va='center', fontsize=8.5,
             color='#555', style='italic')

ax2.text(5, 1.8, "f = décorateur_A(décorateur_B(f))", ha='center',
         fontsize=10, fontfamily='monospace',
         bbox=dict(boxstyle='round,pad=0.3', facecolor='#eee', alpha=0.8))

ax2.annotate('', xy=(5, 2.6), xytext=(5, 3.5),
             arrowprops=dict(arrowstyle='<-', color='#888', lw=1.5))

plt.tight_layout()
plt.show()
_images/20be97bcad3babae5e22222b85603d8dc0ed43e39bff182c4babeca45837be6d.png

Résumé#

Ce chapitre a couvert les décorateurs dans leur globalité, de leurs fondements théoriques à leurs applications pratiques :

  • Les fonctions de première classe et les fermetures sont les deux mécanismes sur lesquels reposent les décorateurs : une fonction peut retourner une autre fonction qui capture des variables de sa portée.

  • Un décorateur simple est une fonction qui prend une fonction et retourne une fonction enveloppe. La syntaxe @décorateur est un sucre syntaxique pour f = décorateur(f).

  • functools.wraps doit toujours être utilisé dans la fonction enveloppe pour préserver les métadonnées (__name__, __doc__, __annotations__, __wrapped__) de la fonction originale.

  • Un décorateur paramétré ajoute un niveau d’imbrication supplémentaire : une fabrique de décorateurs accepte les arguments et retourne le décorateur proprement dit.

  • Les cas d’usage les plus courants sont le cache (functools.lru_cache, functools.cache), la minuterie, la réessai sur erreur, et la validation de types.

  • L”empilement de décorateurs s’effectue de bas en haut : le plus proche de def est appliqué en premier.

  • Les décorateurs de classe transforment une classe entière et sont utilisés notamment pour implémenter le patron de conception Singleton ou pour enregistrer des classes dans un registre.

Dans le chapitre suivant, nous aborderons les modules, paquets et l’outil uv — la façon dont Python organise le code à grande échelle et comment les projets modernes gèrent leurs dépendances.