Les décorateurs : ajouter un comportement à une fonction sans y toucher
- Expliquer ce qu’est un décorateur et comment il est construit à partir d’une fonction d’ordre supérieur et d’une closure
- Écrire un décorateur avec une fonction wrapper et l’appliquer avec @
- Écrire un wrapper adapté à n’importe quelle fonction grâce à *args et **kwargs
- Reconnaître les deux erreurs courantes — un wrapper sans return, et return wrapper() avec des parenthèses
Qu’allons-nous apprendre ?
- Ce qu’est un décorateur (Decorator) et pourquoi il est utile.
- Comment construire un décorateur : une fonction qui reçoit une fonction et renvoie un wrapper (une enveloppe).
- La syntaxe
@. - Un wrapper adapté à n’importe quelle fonction, avec
*argset**kwargs.
Pourquoi en avons-nous besoin ?
Nous voulons afficher un message à chaque appel d’une fonction — pour le débogage :
def add(a, b):
print("Calling add")
return a + b
def multiply(a, b):
print("Calling multiply")
return a * bLes problèmes :
- La même ligne est copiée dans chaque fonction — encore la duplication que DRY interdit.
- La logique se mélange à du code d’aide qui n’a rien à voir avec elle.
- Pour supprimer les affichages, il faut parcourir toutes les fonctions et les effacer.
Un décorateur permet d’écrire le comportement supplémentaire une seule fois, et de le poser sur n’importe quelle fonction en une ligne.
Explication à partir des bases
De quoi est fait un décorateur
Un décorateur combine deux choses apprises dans les leçons précédentes :
- La fonction d’ordre supérieur — il reçoit une fonction et renvoie une fonction.
- La closure — la fonction qu’il renvoie se souvient de la fonction d’origine.
La structure est toujours la même :
- Le décorateur reçoit la fonction d’origine (
func). - À l’intérieur, on définit une nouvelle fonction — le
wrapper— qui fait quelque chose de plus, et appellefunc. - Le décorateur renvoie
wrapper— sans parenthèses.
La syntaxe @
@log_call
def add(a, b):
return a + bC’est exactement comme écrire après la définition :
add = log_call(add)Autrement dit : le nom add pointe maintenant vers le wrapper, et le wrapper appelle l’add d’origine.
Le cadeau (la fonction) ne change pas. Vous l’enveloppez de papier (le wrapper) — et celui qui le reçoit rencontre d’abord l’emballage, et seulement ensuite le cadeau. On peut envelopper n’importe quel cadeau dans le même type de papier, et on peut retirer l’emballage sans abîmer le cadeau.
Syntaxe
def my_decorator(func): # 1. receive the original function
def wrapper(*args, **kwargs): # 2. a new function that wraps it
# ... something before ...
result = func(*args, **kwargs)
# ... something after ...
return result # return the original result
return wrapper # 3. return the wrapper (no parentheses)
@my_decorator # same as: my_function = my_decorator(my_function)
def my_function():
...Premier exemple de code
week-2/log_call.py :
def log_call(func):
def wrapper(a, b):
print("Calling", func.__name__)
return func(a, b)
return wrapper
@log_call
def add(a, b):
return a + b
@log_call
def multiply(a, b):
return a * b
print(add(2, 3))
print(multiply(4, 5))Sortie :
Calling add
5
Calling multiply
20Analyse du code ligne par ligne
| Ligne | Explication |
|---|---|
def log_call(func): | Le décorateur reçoit la fonction d’origine. |
def wrapper(a, b): | L’enveloppe — elle reçoit les mêmes paramètres que l’originale. |
print("Calling", func.__name__) | Le comportement supplémentaire. __name__ est le nom de la fonction. |
return func(a, b) | Appelle l’originale, et renvoie son résultat. |
return wrapper | Le décorateur renvoie le wrapper — sans parenthèses. |
@log_call | add = log_call(add). Désormais add(2, 3) exécute le wrapper. |
Un autre exemple
Un wrapper adapté à n’importe quelle fonction. Le wrapper précédent ne fonctionne qu’avec des fonctions qui reçoivent exactement a, b. Avec *args et **kwargs, il reçoit n’importe quel paramètre et le transmet. week-2/log_any.py :
def log_call(func):
def wrapper(*args, **kwargs):
print("Calling", func.__name__, "with", args, kwargs)
return func(*args, **kwargs)
return wrapper
@log_call
def greet(name, greeting="Hello"):
return greeting + ", " + name
@log_call
def total(*prices):
return sum(prices)
print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))Sortie :
Calling greet with ('Dana',) {'greeting': 'Hi'}
Hi, Dana
Calling total with (10, 20, 30) {}
60Un exemple utile : mesurer le temps. week-2/timed.py :
import time
def timed(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
seconds = round(time.time() - start, 2)
print(func.__name__, "took", seconds, "seconds")
return result
return wrapper
@timed
def slow_sum(n):
time.sleep(0.5)
return sum(range(n))
print(slow_sum(1000))Sortie :
slow_sum took 0.5 seconds
499500(Le temps exact varie un peu d’une exécution à l’autre.) Le même @timed peut être posé sur n’importe quelle fonction du programme — sans changer une seule ligne à l’intérieur.
Erreur courante
Deux erreurs qui arrivent à presque tous ceux qui écrivent un premier décorateur :
def log_call(func):
def wrapper(*args, **kwargs):
print("Calling", func.__name__)
func(*args, **kwargs)
return wrapper()- Pas de
returndans le wrapper. La fonction d’origine s’exécute, mais son résultat est perdu — et la fonction décorée renvoie toujoursNone. La correction :return func(*args, **kwargs). return wrapper()avec des parenthèses. Cela appelle le wrapper dès la définition, et renvoie son résultat au lieu de la fonction. La correction :return wrapper.
La règle : le wrapper renvoie le résultat, et le décorateur renvoie le wrapper.
Exercices
Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.
Exercice 1: Un premier décorateur
Écrivez un décorateur nommé log_call qui affiche Calling <name> avant chaque appel à la fonction, et appliquez-le avec @ à add et à multiply.
print(add(2, 3))
print(multiply(4, 5))Résultat attendu :
Calling add
5
Calling multiply
20Afficher la solution
def log_call(func):
def wrapper(a, b):
print("Calling", func.__name__)
return func(a, b)
return wrapper
@log_call
def add(a, b):
return a + b
@log_call
def multiply(a, b):
return a * b
print(add(2, 3))
print(multiply(4, 5))Calling add
5
Calling multiply
20@log_call au-dessus de def add équivaut à add = log_call(add). Désormais add pointe vers le wrapper.
Exercice 2: Un wrapper adapté à n’importe quelle fonction
Le wrapper précédent ne fonctionne qu’avec des fonctions qui reçoivent exactement deux paramètres. Réécrivez log_call pour qu’il fonctionne avec n’importe quelle fonction.
@log_call
def greet(name, greeting="Hello"):
return greeting + ", " + name
@log_call
def total(*prices):
return sum(prices)
print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))Résultat attendu :
Calling greet
Hi, Dana
Calling total
60Afficher la solution
def log_call(func):
def wrapper(*args, **kwargs):
print("Calling", func.__name__)
return func(*args, **kwargs)
return wrapper
@log_call
def greet(name, greeting="Hello"):
return greeting + ", " + name
@log_call
def total(*prices):
return sum(prices)
print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))Calling greet
Hi, Dana
Calling total
60*args et **kwargs rassemblent tous les arguments, et func(*args, **kwargs) les transmet tels quels.
Exercice 3: Vérifier l’entrée
Écrivez un décorateur nommé positive_only pour des fonctions qui reçoivent un seul nombre : si le nombre est négatif, il lance une ValueError et n’appelle pas du tout la fonction. Le message contient le nom de la fonction (func.__name__) et le nombre reçu, comme dans le résultat attendu.
@positive_only
def square_root(n):
return n ** 0.5
print(square_root(16))
try:
square_root(-4)
except ValueError as error:
print("error:", error)Résultat attendu :
4.0
error: square_root needs a positive number, got -4Afficher la solution
def positive_only(func):
def wrapper(n):
if n < 0:
raise ValueError(func.__name__ + " needs a positive number, got " + str(n))
return func(n)
return wrapper
@positive_only
def square_root(n):
return n ** 0.5
print(square_root(16))
try:
square_root(-4)
except ValueError as error:
print("error:", error)4.0
error: square_root needs a positive number, got -4La vérification a lieu avant l’appel à func — si l’entrée est invalide, la fonction d’origine ne s’exécute pas du tout.
Exercice 4: Compter les appels
Écrivez un décorateur nommé count_calls, qui compte combien de fois la fonction a été appelée. À chaque appel, avant l’exécution de la fonction, il affiche le nom de la fonction et le numéro de l’appel — comme dans le résultat attendu.
@count_calls
def say_hi():
print("Hi")
say_hi()
say_hi()Résultat attendu :
say_hi call #1
Hi
say_hi call #2
HiAfficher la solution
def count_calls(func):
count = 0
def wrapper(*args, **kwargs):
nonlocal count
count += 1
print(func.__name__, "call #" + str(count))
return func(*args, **kwargs)
return wrapper
@count_calls
def say_hi():
print("Hi")
say_hi()
say_hi()say_hi call #1
Hi
say_hi call #2
Hicount appartient à count_calls, et le wrapper s’en souvient (une closure). Comme il modifie count, il a besoin de nonlocal.
Exercice 5: Deux erreurs
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
result.upper()
return wrapper()Le décorateur est censé renvoyer le résultat de la fonction en majuscules. Trouvez deux erreurs et corrigez-les.
@shout
def greet(name):
return "hello " + name
print(greet("dana"))Résultat attendu :
HELLO DANAAfficher la solution
return wrapper()avec des parenthèses — appelle le wrapper dès la définition, au lieu de le renvoyer. Il fautreturn wrapper.- Le wrapper ne renvoie rien —
result.upper()crée une nouvelle chaîne, mais elle n’est pas renvoyée. Il fautreturn result.upper().
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
return "hello " + name
print(greet("dana"))HELLO DANALa règle : le wrapper renvoie le résultat, et le décorateur renvoie le wrapper.
Résumé
- Décorateur = une fonction qui reçoit une fonction, l’enveloppe dans un wrapper avec un comportement supplémentaire, et renvoie le wrapper.
- Il est construit à partir d’une fonction d’ordre supérieur et d’une closure — les deux sujets des leçons précédentes.
@decoratorau-dessus d’undef=func = decorator(func).- Avec
*argset**kwargs, le wrapper s’adapte à n’importe quelle fonction. - Le wrapper renvoie le résultat (
return func(...)), et le décorateur renvoie le wrapper (return wrapper, sans parenthèses).