Apprenez en lisant dans l'ordre

Décorateurs — ajouter du comportement aux fonctions avec @

Apprends à utiliser les décorateurs Python avec @ pour ajouter du comportement à tes fonctions sans modifier leur code.

À la fin de l'article précédent sur les lambdas, tu avais vu les principales façons de traiter les fonctions comme des valeurs. Pour conclure, fixons les décorateurs — la syntaxe dédiée pour superposer un comportement supplémentaire à une fonction.

Qu'est-ce qu'un décorateur ?

Un décorateur est une manière d'ajouter un comportement avant et après une fonction sans modifier la fonction elle-même. Des choses comme « logger l'appel », « chronométrer l'exécution » ou « mettre le résultat en cache » — du comportement partagé que tu veux superposer à plein de fonctions — peuvent vivre à un seul endroit.

La syntaxe tient en une ligne au-dessus de la définition de fonction : @nom_decorateur. Python lit ça comme « la même chose que func = nom_decorateur(func) ».

@ signifie « passe cette fonction par cette autre »
@loggerdef greet():greet = logger(greet)se développe en
Écrire @logger au-dessus de def greet(): fait exécuter à Python greet = logger(greet) en interne, remplaçant greet par une nouvelle fonction enveloppée par logger.
# Le décorateur lui-même (une fonction d'ordre supérieur qui prend et retourne une fonction)
def logger(func):
    def wrapper():
        print("=== début ===")
        func()                       # appelle la fonction d'origine
        print("=== fin ===")
    return wrapper

# Côté appelant : ajoute juste @
@logger
def greet(): # → logger(greet)
    print("Bonjour")

greet()
# === début ===
# Bonjour
# === fin ===

# En interne, équivalent à :
# def greet():
#     print("Bonjour")
# greet = logger(greet)

Le décorateur de base — envelopper une fonction avec wrapper

Le squelette d'un décorateur est une forme en 3 étapes : la fonction externe prend func, la fonction interne (par convention wrapper) appelle func(), et tu fais return wrapper. Le wrapper qui garde func accessible pendant qu'il s'exécute est exactement une clôture.

Tout ce que tu écris avant et après func() s'exécute à chaque appel de la fonction décorée.

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"[LOG] exécution de {func.__name__}")
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} terminé")
        return result
    return wrapper

@logger
def greet(name):
    return f"Bonjour, {name}"

print(greet("Alice"))
# [LOG] exécution de greet
# [LOG] greet terminé
# Bonjour, Alice
Comment le décorateur logger enveloppe une fonction
Module (espace de noms global)
  • greet est remplacée par la fonction wrapper
  • Le corps original de greet survit comme func dans wrapper
Frame de logger(func)
  • func contient la greet originale
  • Construit wrapper à l'intérieur et le retourne
wrapper (une clôture qui se souvient de func)
  • Exécute pré → func() → post dans l'ordre
  • De l'extérieur, c'est la nouvelle greet
Ce que fait @logger, c'est échanger la greet globale contre une fonction différente appelée wrapper. Le corps original de greet est appelé sous le nom func depuis l'intérieur de wrapper.

Construis un décorateur bracket qui affiche des salutations avant et après une fonction et superpose-le.

① Définis def bracket(func):, et à l'intérieur def wrapper():. Fais que wrapper exécute print("--- début ---")func()print("--- fin ---") dans l'ordre, et que la fonction externe retourne wrapper.

② Définis def introduce(): décorée avec @bracket, avec juste print("Je suis Alice") dans le corps.

③ Appelle introduce() et vérifie que le corps est encadré entre --- début --- / --- fin ---.

(Lorsque la réponse est correcte, l'explication apparaîtra.)

Éditeur Python

Exécuter le code pour voir le résultat

Passer n'importe quels arguments avec *args / **kwargs

Jusqu'à présent, wrapper ne prenait pas d'arguments. Quand tu veux décorer des fonctions qui prennent des arguments, utilise *args / **kwargs pour accepter n'importe quels arguments tels quels et les transmettre directement à `func`.

Ça transforme le décorateur en un décorateur générique qui marche avec n'importe quelle signature de fonction. Il gère add(2, 3) (positionnel) et add(2, 3, name="ABC") (nommé) avec le même décorateur.

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"appel : args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)   # dépaquette et transmet ici aussi
        print(f"résultat : {result}")
        return result                    # n'oublie pas de le retourner
    return wrapper

@log_call
def add(a, b):
    return a + b

print(add(2, 3))
# appel : args=(2, 3), kwargs={}
# résultat : 5
# 5

print(add(2, b=3))
# appel : args=(2,), kwargs={'b': 3}
# résultat : 5
# 5
Comment *args / **kwargs transmettent les arguments tels quels
add(2, b=3)wrapper(*args, **kwargs)args=(2,), kwargs={'b': 3}dépaquette et transmeten func(*args, **kwargs)add(a, b)add d'origine(a=2, b=3)retourne 5wrapper le retournetel quelreçoitpré-étapedépaquettecalculerenvoie le résultat

N'oublie pas de return le résultat

Si wrapper n'écrit que result = func(...) et oublie le return, la valeur de retour de la fonction décorée se transforme silencieusement en None. L'accident classique, c'est de retrouver add(2, 3) qui retourne discrètement None — quand tu écris un décorateur, traite return result comme faisant partie de la même mémoire musculaire.

Construis un décorateur log_call qui affiche chaque appel et applique-le à une fonction à 2 arguments.

① Définis def log_call(func):, et à l'intérieur def wrapper(*args, **kwargs):.

② Dans wrapper, affiche f"appel : args={args}, kwargs={kwargs}", puis result = func(*args, **kwargs), puis return result.

③ Fais que la fonction externe retourne wrapper.

④ Définis def multiply(a, b): return a * b décorée avec @log_call. Appelle print(multiply(4, 5)) et print(multiply(2, b=10)) et vérifie que le log apparaît suivi de la valeur de retour.

Éditeur Python

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Laquelle des propositions suivantes a le même sens que mettre @logger au-dessus de def greet(): ... ?

Question 2Qu'affiche ce code ?
def deco(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@deco
def plus(a, b):
return a + b
print(plus(3, 4))