43  Les décorateurs

Les décorateurs sont une des fonctionnalités les plus emblématiques de Python. On en a déjà rencontré plusieurs : @property, @staticmethod, @dataclass, @classmethod. Vous allez maintenant comprendre comment ils fonctionnent et comment écrire les vôtres. C’est un passage obligé pour le niveau Expert du TOSA.

🎯 Pré-requis

Ce chapitre suppose que vous maîtrisez les fonctions et *args / **kwargs (Partie 3 chapitre 2). Si vous hésitez, révisez avant.

43.1 Les fonctions de premier ordre

En Python, les fonctions sont des objets comme les autres. On peut :

  • Les stocker dans une variable.
  • Les passer en argument à une autre fonction.
  • Les renvoyer depuis une autre fonction.
  • Les mettre dans une liste ou un dict.

Stocker dans une variable

def saluer(nom):
    return f"Bonjour {nom}"

# Attribuer la fonction à une autre variable
hello = saluer              # attention : pas de parenthèses !

print(hello("Alice"))
print(hello is saluer)       # même objet
Bonjour Alice
True

Passer en argument

def appliquer_deux_fois(fonction, x):
    return fonction(fonction(x))


def ajouter_5(n):
    return n + 5


print(appliquer_deux_fois(ajouter_5, 10))     # 10 → 15 → 20
20

On appelle ça une fonction d’ordre supérieur. Vous en avez déjà utilisé : sorted(..., key=...), filter(fonction, ...), map(fonction, ...).

Renvoyer une fonction

Une fonction peut créer et renvoyer une autre fonction.

def multiplicateur(facteur):
    """Renvoie une fonction qui multiplie par facteur."""
    def multiplier(x):
        return x * facteur
    return multiplier


doubler = multiplicateur(2)
tripler = multiplicateur(3)

print(doubler(5))
print(tripler(5))
10
15

43.2 Les closures

Regardez attentivement l’exemple ci-dessus : comment multiplier connaît-il la valeur de facteur alors que multiplicateur a fini son exécution ?

Réponse : c’est une closure (fermeture lexicale). La fonction interne capture les variables de la fonction englobante.

def compteur():
    """Renvoie une fonction qui compte les appels."""
    n = 0
    def incrementer():
        nonlocal n                # ← pour pouvoir modifier n
        n += 1
        return n
    return incrementer


c1 = compteur()
c2 = compteur()

print(c1())     # 1
print(c1())     # 2
print(c1())     # 3
print(c2())     # 1 (compteur indépendant)
1
2
3
1

Le mot-clé nonlocal

Dans une fonction imbriquée, lire une variable englobante est automatique. Mais la modifier nécessite nonlocal :

def externe():
    x = 10
    def interne():
        x += 1        # ❌ erreur : Python croit que x est local
    interne()

externe()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Cell In[5], line 7
      3     def interne():
      4         x += 1        # ❌ erreur : Python croit que x est local
      5     interne()
      6 
----> 7 externe()

Cell In[5], line 5, in externe()
      1 def externe():
      2     x = 10
      3     def interne():
      4         x += 1        # ❌ erreur : Python croit que x est local
----> 5     interne()

Cell In[5], line 4, in externe.<locals>.interne()
      3     def interne():
----> 4         x += 1        # ❌ erreur : Python croit que x est local

UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
def externe():
    x = 10
    def interne():
        nonlocal x       # ✅ explicite que x est dans le scope englobant
        x += 1
        return x
    return interne()


print(externe())
11
nonlocal vs global
  • global x : la variable est dans le scope global du module.
  • nonlocal x : la variable est dans un scope englobant (pas global, pas local).

On reviendra sur les scopes au chapitre suivant (LEGB).

43.3 Qu’est-ce qu’un décorateur ?

Un décorateur est une fonction qui prend une fonction en argument et renvoie une nouvelle fonction qui étend ou modifie le comportement de l’originale.

Sans décorateur (à la main)

def tracer(fonction):
    """Affiche le nom et les arguments de la fonction, puis l'exécute."""
    def wrapper(*args, **kwargs):
        print(f">>> {fonction.__name__}({args}, {kwargs})")
        resultat = fonction(*args, **kwargs)
        print(f"    = {resultat}")
        return resultat
    return wrapper


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


# Manuellement : on remplace additionner par sa version tracée
additionner = tracer(additionner)

additionner(3, 5)
additionner(10, 20)
>>> additionner((3, 5), {})
    = 8
>>> additionner((10, 20), {})
    = 30
30

Avec la syntaxe @

La syntaxe @tracer au-dessus d’une fonction fait la même chose automatiquement :

def tracer(fonction):
    def wrapper(*args, **kwargs):
        print(f">>> {fonction.__name__}({args}, {kwargs})")
        resultat = fonction(*args, **kwargs)
        print(f"    = {resultat}")
        return resultat
    return wrapper


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


# @tracer équivaut à : additionner = tracer(additionner)
additionner(3, 5)
>>> additionner((3, 5), {})
    = 8
8

C’est tout : @decorateur est du sucre syntaxique pour fonction = decorateur(fonction).

43.4 Anatomie d’un décorateur

Structure générique d’un décorateur :

def mon_decorateur(fonction):
    def wrapper(*args, **kwargs):
        # avant l'appel
        resultat = fonction(*args, **kwargs)
        # après l'appel
        return resultat
    return wrapper

Points à comprendre :

  1. fonction : la fonction décorée (ex: additionner).
  2. wrapper : la nouvelle fonction qu’on renvoie à la place.
  3. *args, **kwargs : pour supporter toutes les signatures possibles.
  4. On renvoie wrapper — c’est elle qui remplace fonction.

43.5 Exemples de décorateurs utiles

1. Mesurer le temps d’exécution

import time

def chronometre(fonction):
    def wrapper(*args, **kwargs):
        debut = time.time()
        resultat = fonction(*args, **kwargs)
        duree = time.time() - debut
        print(f"{fonction.__name__} exécutée en {duree*1000:.1f} ms")
        return resultat
    return wrapper


@chronometre
def calcul_long(n):
    return sum(i ** 2 for i in range(n))


r = calcul_long(1_000_000)
print(f"Résultat : {r:,}")
calcul_long exécutée en 73.9 ms
Résultat : 333,332,833,333,500,000

2. Mise en cache simple

def cacher(fonction):
    cache = {}

    def wrapper(*args):
        if args not in cache:
            cache[args] = fonction(*args)
        else:
            print(f"  [cache hit pour {args}]")
        return cache[args]

    return wrapper


@cacher
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)


print(fib(10))
print(fib(10))        # 2e appel = cache hit
print(fib(5))         # déjà calculé via fib(10)
  [cache hit pour (1,)]
  [cache hit pour (2,)]
  [cache hit pour (3,)]
  [cache hit pour (4,)]
  [cache hit pour (5,)]
  [cache hit pour (6,)]
  [cache hit pour (7,)]
  [cache hit pour (8,)]
55
  [cache hit pour (10,)]
55
  [cache hit pour (5,)]
5
@functools.cache fait ça pour vous

Python propose ce décorateur tout prêt :

from functools import cache

@cache
def fib(n):
    return n if n < 2 else fib(n-1) + fib(n-2)


print(fib(100))       # instantané grâce au cache
354224848179261915075

Pour une version avec taille limite, utilisez @lru_cache(maxsize=128).

3. Vérifier les types d’arguments

def types_stricts(**types_attendus):
    """Décorateur paramétré qui vérifie les types."""
    def decorateur(fonction):
        def wrapper(*args, **kwargs):
            # Vérifier les kwargs
            for nom, type_attendu in types_attendus.items():
                if nom in kwargs and not isinstance(kwargs[nom], type_attendu):
                    raise TypeError(
                        f"{nom} doit être {type_attendu.__name__}, "
                        f"pas {type(kwargs[nom]).__name__}"
                    )
            return fonction(*args, **kwargs)
        return wrapper
    return decorateur


@types_stricts(nom=str, age=int)
def creer_utilisateur(nom, age):
    return f"{nom}, {age} ans"


print(creer_utilisateur(nom="Alice", age=30))
Alice, 30 ans
@types_stricts(nom=str, age=int)
def creer_utilisateur(nom, age):
    return f"{nom}, {age} ans"

creer_utilisateur(nom="Alice", age="trente")     # TypeError
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[13], line 5
      1 @types_stricts(nom=str, age=int)
      2 def creer_utilisateur(nom, age):
      3     return f"{nom}, {age} ans"
      4 
----> 5 creer_utilisateur(nom="Alice", age="trente")     # TypeError

Cell In[12], line 8, in types_stricts.<locals>.decorateur.<locals>.wrapper(*args, **kwargs)
      4         def wrapper(*args, **kwargs):
      5             # Vérifier les kwargs
      6             for nom, type_attendu in types_attendus.items():
      7                 if nom in kwargs and not isinstance(kwargs[nom], type_attendu):
----> 8                     raise TypeError(
      9                         f"{nom} doit être {type_attendu.__name__}, "
     10                         f"pas {type(kwargs[nom]).__name__}"
     11                     )

TypeError: age doit être int, pas str

43.6 Préserver l’identité avec @functools.wraps

Un piège classique : quand on décore une fonction, son nom et sa docstring disparaissent.

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


@tracer
def additionner(a, b):
    """Additionne deux nombres."""
    return a + b


print(additionner.__name__)    # 'wrapper' !
print(additionner.__doc__)     # None !
wrapper
None

Solution : utiliser @functools.wraps dans votre décorateur pour préserver les métadonnées.

from functools import wraps

def tracer(fonction):
    @wraps(fonction)                  # préserve __name__, __doc__, etc.
    def wrapper(*args, **kwargs):
        return fonction(*args, **kwargs)
    return wrapper


@tracer
def additionner(a, b):
    """Additionne deux nombres."""
    return a + b


print(additionner.__name__)    # 'additionner' ✅
print(additionner.__doc__)     # 'Additionne deux nombres.' ✅
additionner
Additionne deux nombres.
Toujours utiliser @wraps

Sans @wraps, votre code a l’air de fonctionner, mais :

  • Le débogage devient pénible (toutes les fonctions s’appellent wrapper).
  • Certains outils d’introspection (tests, docs auto, type checkers) se cassent.

Règle d’or : @wraps(fonction) sur chaque wrapper. C’est une ligne quasi gratuite.

43.7 Décorateurs avec arguments

Un décorateur « simple » prend une fonction. Pour lui passer des arguments (comme @types_stricts(...) vu plus haut), il faut une fabrique de décorateurs — trois niveaux d’imbrication.

def decorateur_avec_args(argument):         # niveau 1 : reçoit l'argument
    def decorateur(fonction):               # niveau 2 : reçoit la fonction
        @wraps(fonction)
        def wrapper(*args, **kwargs):       # niveau 3 : l'appel réel
            # utiliser argument et appeler fonction
            return fonction(*args, **kwargs)
        return wrapper
    return decorateur

Exemple : décorateur repeter(n)

from functools import wraps

def repeter(n):
    """Répète l'appel n fois, renvoie une liste des résultats."""
    def decorateur(fonction):
        @wraps(fonction)
        def wrapper(*args, **kwargs):
            return [fonction(*args, **kwargs) for _ in range(n)]
        return wrapper
    return decorateur


@repeter(3)
def dire_bonjour():
    return "Bonjour !"


print(dire_bonjour())
['Bonjour !', 'Bonjour !', 'Bonjour !']

Quand on écrit @repeter(3)

Python interprète : dire_bonjour = repeter(3)(dire_bonjour).

  1. repeter(3) renvoie decorateur (avec n=3 capturé en closure).
  2. decorateur(dire_bonjour) renvoie wrapper.
  3. dire_bonjour devient ce wrapper.

43.8 Empiler plusieurs décorateurs

On peut empiler plusieurs décorateurs. Ils s’appliquent de bas en haut.

from functools import wraps

def majuscules(fonction):
    @wraps(fonction)
    def wrapper(*args, **kwargs):
        return fonction(*args, **kwargs).upper()
    return wrapper


def exclamation(fonction):
    @wraps(fonction)
    def wrapper(*args, **kwargs):
        return fonction(*args, **kwargs) + " !!!"
    return wrapper


@majuscules
@exclamation
def saluer(nom):
    return f"Bonjour {nom}"


print(saluer("Alice"))
BONJOUR ALICE !!!

Ordre d’application : saluerexclamation(saluer)majuscules(exclamation(saluer)).

Donc @majuscules (le plus haut) s’applique en dernier (en externe).

43.9 Décorateurs de classes

La syntaxe @ marche aussi sur les classes. Vous en avez déjà vu : @dataclass.

def ajouter_info(cls):
    cls.version = "1.0"
    cls.decouvert = True
    return cls


@ajouter_info
class Produit:
    def __init__(self, nom):
        self.nom = nom


p = Produit("Widget")
print(p.nom)
print(Produit.version)
print(Produit.decouvert)
Widget
1.0
True

C’est la même mécanique : Produit = ajouter_info(Produit).

43.10 Décorateurs de la bibliothèque standard

Python en fournit de très utiles dans functools :

Décorateur Rôle
@functools.wraps Préserve les métadonnées du décoré
@functools.cache Cache illimité (Python 3.9+)
@functools.lru_cache(maxsize=128) Cache avec taille maximale
@functools.total_ordering Génère les opérateurs de comparaison à partir de == et <
@functools.singledispatch Dispatch selon le type du 1er argument

Autres classiques :

Décorateur Rôle
@staticmethod Méthode qui n’utilise ni self ni cls
@classmethod Méthode qui reçoit cls
@property Méthode qui se comporte comme un attribut
@dataclass Génère __init__, __repr__, __eq__

@staticmethod et @classmethod

On les a survolés en Partie 3. Précisons :

class Calculatrice:

    @staticmethod
    def carre(x):
        """N'a pas besoin de self ni de cls."""
        return x ** 2

    @classmethod
    def depuis_deux_nombres(cls, a, b):
        """Constructeur alternatif : reçoit la classe."""
        instance = cls()
        instance.resultat = a + b
        return instance


# Appel sans instance
print(Calculatrice.carre(5))

# Constructeur alternatif
c = Calculatrice.depuis_deux_nombres(3, 4)
print(c.resultat)
25
7

Distinction :

  • @staticmethod : pas de paramètre implicite. Juste une fonction dans le namespace de la classe.
  • @classmethod : reçoit cls (la classe elle-même). Utile pour des constructeurs alternatifs.
  • Méthode classique : reçoit self (l’instance).

🧩 Quiz 3.1 — Décorateurs

Question 1

Que fait la syntaxe @decorateur au-dessus d’une fonction ?

  1. Met la fonction en cache
  2. Équivaut à f = decorateur(f)
  3. Affiche la fonction
  4. Crée une copie

b)@decorateur est du sucre syntaxique pour remplacer f par le résultat de decorateur(f).

Question 2

Qu’est-ce qu’une closure ?

  1. Une instruction pour fermer un fichier
  2. Une fonction interne qui capture des variables du scope englobant
  3. Un type d’exception
  4. Une variable globale

b) — la closure « enferme » les variables du scope externe, leur donnant une durée de vie au-delà de la fonction englobante.

Question 3

Que fait le mot-clé nonlocal ?

  1. Déclare une variable globale
  2. Permet de modifier une variable du scope englobant (ni local, ni global)
  3. Rend la variable privée
  4. Rien, c’est juste décoratif

b) — sans nonlocal, une affectation dans une fonction interne crée une nouvelle variable locale. nonlocal signale à Python de modifier la variable du scope englobant.

Question 4

Pourquoi utiliser @functools.wraps ?

  1. Pour améliorer les performances
  2. Pour préserver __name__, __doc__ et autres métadonnées de la fonction décorée
  3. Pour rendre le décorateur obligatoire
  4. Pour activer le mode debug

b) — sans @wraps, toutes vos fonctions décorées apparaissent comme wrapper dans le débogage et les introspections.

Question 5

Dans quel ordre s’appliquent les décorateurs empilés ?

@A
@B
def f(): pass
  1. A puis B
  2. B puis A
  3. Peu importe
  4. Dans l’ordre alphabétique

b) B puis A — équivalent à f = A(B(f)). Le plus proche de la fonction (@B) est appliqué en premier.

Question 6

Un décorateur paramétré est :

  1. Une fonction qui renvoie un décorateur
  2. Un décorateur qui ne prend aucun argument
  3. Un décorateur pour classes uniquement
  4. Impossible en Python

a) — trois niveaux : decorator_factory(args) → decorator(f) → wrapper(...).

Question 7

Que fait @functools.cache ?

  1. Enregistre les résultats dans un fichier
  2. Met en cache les appels (mémoïsation)
  3. Vide le cache
  4. C’est un synonyme de @property

b)mémoïsation : si la fonction est appelée avec les mêmes arguments, le résultat est réutilisé sans recalcul. Très utile pour les fonctions récursives lourdes (Fibonacci, etc.).

Question 8

Que s’affiche-t-il ?

from functools import wraps

def double(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs) * 2
    return wrapper

@double
def valeur():
    return 5

print(valeur())
  1. 5
  2. 10
  3. 55
  4. Une erreur

b) 10 — le wrapper appelle valeur() qui renvoie 5, puis multiplie par 2.

from functools import wraps

def double(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs) * 2
    return wrapper

@double
def valeur():
    return 5

print(valeur())
10

✏️ Exercice 3.1 — Décorateur @logger

Écrivez un décorateur @logger qui affiche "APPEL : nom_fonction(args, kwargs)" avant chaque appel, et "RETOUR : valeur" après.

from functools import wraps

def logger(fonction):
    ...

@logger
def addition(a, b):
    return a + b

@logger
def saluer(nom, politesse="Bonjour"):
    return f"{politesse}, {nom}"

addition(3, 5)
saluer("Alice")
saluer("Bob", politesse="Salut")
from functools import wraps

def logger(fonction):
    @wraps(fonction)
    def wrapper(*args, **kwargs):
        # Formatage des arguments pour affichage
        args_str = ", ".join(repr(a) for a in args)
        kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
        signature = ", ".join(filter(None, [args_str, kwargs_str]))

        print(f"APPEL  : {fonction.__name__}({signature})")
        resultat = fonction(*args, **kwargs)
        print(f"RETOUR : {resultat!r}")
        return resultat
    return wrapper


@logger
def addition(a, b):
    return a + b


@logger
def saluer(nom, politesse="Bonjour"):
    return f"{politesse}, {nom}"


addition(3, 5)
print()
saluer("Alice")
print()
saluer("Bob", politesse="Salut")
APPEL  : addition(3, 5)
RETOUR : 8

APPEL  : saluer('Alice')
RETOUR : 'Bonjour, Alice'

APPEL  : saluer('Bob', politesse='Salut')
RETOUR : 'Salut, Bob'
'Salut, Bob'

✏️ Exercice 3.2 — Décorateur @timer avec seuil

Écrivez un décorateur paramétré @alerte_lente(seuil) qui affiche un message si et seulement si la fonction met plus de seuil secondes à s’exécuter.

import time
from functools import wraps

def alerte_lente(seuil):
    ...

@alerte_lente(0.1)
def calcul_rapide():
    return 42

@alerte_lente(0.1)
def calcul_lent():
    time.sleep(0.2)
    return "fini"

calcul_rapide()      # pas d'alerte
calcul_lent()        # alerte
import time
from functools import wraps

def alerte_lente(seuil):
    def decorateur(fonction):
        @wraps(fonction)
        def wrapper(*args, **kwargs):
            debut = time.time()
            resultat = fonction(*args, **kwargs)
            duree = time.time() - debut
            if duree > seuil:
                print(f"⚠️ {fonction.__name__} lente : {duree*1000:.0f} ms (seuil {seuil*1000:.0f} ms)")
            return resultat
        return wrapper
    return decorateur


@alerte_lente(0.1)
def calcul_rapide():
    return 42


@alerte_lente(0.1)
def calcul_lent():
    time.sleep(0.2)
    return "fini"


print(calcul_rapide())
print(calcul_lent())
42
⚠️ calcul_lent lente : 201 ms (seuil 100 ms)
fini

✏️ Exercice 3.3 — Décorateur @retry

Écrivez un décorateur @retry(n_essais) qui réessaie une fonction en cas d’exception, jusqu’à n_essais fois.

import random
from functools import wraps

def retry(n_essais):
    ...

@retry(3)
def operation_instable():
    if random.random() < 0.7:
        raise ValueError("Échec aléatoire")
    return "succès"

operation_instable()
import random
from functools import wraps

def retry(n_essais):
    def decorateur(fonction):
        @wraps(fonction)
        def wrapper(*args, **kwargs):
            derniere_exception = None
            for essai in range(1, n_essais + 1):
                try:
                    return fonction(*args, **kwargs)
                except Exception as e:
                    derniere_exception = e
                    print(f"  [Essai {essai}/{n_essais} échoué : {e}]")
            raise derniere_exception
        return wrapper
    return decorateur


random.seed(42)

@retry(5)
def operation_instable():
    if random.random() < 0.7:
        raise ValueError("Échec aléatoire")
    return "succès"


try:
    print("Résultat :", operation_instable())
except Exception as e:
    print(f"Abandonné après 5 essais : {e}")
  [Essai 1/5 échoué : Échec aléatoire]
  [Essai 2/5 échoué : Échec aléatoire]
  [Essai 3/5 échoué : Échec aléatoire]
  [Essai 4/5 échoué : Échec aléatoire]
Résultat : succès

Variante avec délai entre essais

import random
import time
from functools import wraps

def retry(n_essais, delai=0.1):
    def decorateur(fonction):
        @wraps(fonction)
        def wrapper(*args, **kwargs):
            for essai in range(1, n_essais + 1):
                try:
                    return fonction(*args, **kwargs)
                except Exception as e:
                    print(f"  [Essai {essai} échoué : {e}]")
                    if essai < n_essais:
                        time.sleep(delai)
            raise RuntimeError(f"Abandonné après {n_essais} essais")
        return wrapper
    return decorateur

✏️ Exercice 3.4 — Décorateur @deprecated

Créez @deprecated(message) qui affiche un avertissement avant chaque appel, pour marquer une fonction obsolète.

from functools import wraps
import warnings

def deprecated(message=""):
    ...

@deprecated("Utilisez nouvelle_fonction() à la place")
def ancienne_fonction():
    return "hello"

ancienne_fonction()
from functools import wraps
import warnings


def deprecated(message=""):
    def decorateur(fonction):
        @wraps(fonction)
        def wrapper(*args, **kwargs):
            full_msg = f"{fonction.__name__}() est dépréciée"
            if message:
                full_msg += f". {message}"
            warnings.warn(full_msg, DeprecationWarning, stacklevel=2)
            return fonction(*args, **kwargs)
        return wrapper
    return decorateur


# Activer l'affichage des DeprecationWarning (par défaut elles sont masquées)
warnings.simplefilter("always", DeprecationWarning)


@deprecated("Utilisez nouvelle_fonction() à la place")
def ancienne_fonction():
    return "hello"


import warnings
with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter("always")
    ancienne_fonction()
    if w:
        print(f"⚠️  {w[-1].category.__name__}: {w[-1].message}")
⚠️  DeprecationWarning: ancienne_fonction() est dépréciée. Utilisez nouvelle_fonction() à la place

Points à retenir

  • warnings.warn est préférable à print pour les avertissements : on peut les filtrer, les rediriger, les transformer en erreurs pour les tests…
  • DeprecationWarning est la classe standard pour signaler qu’une fonctionnalité est obsolète.
  • Ce pattern est très utilisé dans les bibliothèques qui évoluent.

À retenir

Points clés du chapitre
  1. Les fonctions sont des objets : on peut les stocker, les passer, les renvoyer.
  2. Une closure est une fonction interne qui capture les variables du scope englobant.
  3. nonlocal pour modifier une variable du scope englobant (global pour le global).
  4. @decorateur est du sucre syntaxique pour f = decorateur(f).
  5. Pattern canonique : def wrapper(*args, **kwargs): ... return fonction(*args, **kwargs).
  6. Toujours @functools.wraps(fonction) pour préserver les métadonnées.
  7. Décorateur paramétré : 3 niveaux imbriqués (factory → decorator → wrapper).
  8. Empilage : @A @B f = f = A(B(f)). Le plus proche s’applique en premier.
  9. functools : wraps, cache, lru_cache, total_ordering, singledispatch.
  10. Aussi @staticmethod, @classmethod, @property (vus en POO).

← Chapitre précédent : Itérateurs et générateursChapitre suivant : Scopes et closures →