Prépa Python

Expert — chapitre 43 sur 54

Les décorateurs

Fonctions de premier ordre, closures et @decorator

70 minAbonnés

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.

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

python
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

Passer en argument

python
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

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.

python
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))

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.

python
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)

Le mot-clé nonlocal

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

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

externe()
python
def externe():
    x = 10
    def interne():
        nonlocal x       # ✅ explicite que x est dans le scope englobant
        x += 1
        return x
    return interne()

print(externe())

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)

python
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)

Avec la syntaxe @

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

python
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)

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

Anatomie d'un décorateur

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

python
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.

Suite réservée aux abonnés

Débloquez l'intégralité du parcours

Les 54 chapitres, les exercices corrigés automatiquement, les examens blancs et le suivi de votre progression.

Voir les offresChapitres en accès libre

Déjà un accès ? Connectez-vous.