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
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 objetPasser 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 → 20On 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))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)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()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)
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 :
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 :
def mon_decorateur(fonction):
def wrapper(*args, **kwargs):
# avant l'appel
resultat = fonction(*args, **kwargs)
# après l'appel
return resultat
return wrapperPoints à comprendre :
fonction: la fonction décorée (ex:additionner).wrapper: la nouvelle fonction qu'on renvoie à la place.*args, **kwargs: pour supporter toutes les signatures possibles.- On renvoie
wrapper— c'est elle qui remplacefonction.