Skip to main content
Niveau : Avancé

Module : Les Décorateurs

1. Quoi : Les Décorateurs

Un décorateur en Python est une fonction qui prend une autre fonction en argument, lui ajoute des fonctionnalités, et retourne une nouvelle fonction (ou la fonction originale modifiée) sans altérer le code source de la fonction de départ.

C'est un concept de méta-programmation : du code qui manipule du code.

La syntaxe avec le @ (le "sucre syntaxique") est la manière la plus courante de les utiliser.

@my_decorator
def my_function():
print("Hello")

est équivalent à :

def my_function():
print("Hello")

my_function = my_decorator(my_function)

2. Pourquoi : Ajouter des fonctionnalités transversales

Les décorateurs sont parfaits pour ajouter des fonctionnalités dites "transversales" (cross-cutting concerns) à de multiples fonctions sans dupliquer de code.

  • Logging : Journaliser les appels de fonction, leurs arguments et leurs résultats.
  • Mesure de performance : Calculer le temps d'exécution d'une fonction.
  • Mise en cache : Mémoriser le résultat d'une fonction pour des appels futurs avec les mêmes arguments.
  • Contrôle d'accès : Vérifier si un utilisateur a les droits nécessaires avant d'exécuter une fonction (très courant dans les frameworks web comme Flask ou Django).
  • Validation de données.

3. Comment : Créer un Décorateur

Un décorateur est généralement implémenté comme une fonction qui définit et retourne une autre fonction (une "closure").

Structure d'un décorateur simple :

  1. Une fonction externe (le décorateur) qui prend une fonction func comme argument.
  2. Une fonction interne (wrapper) qui sera la nouvelle fonction. C'est ici qu'on ajoute la nouvelle logique.
  3. Le wrapper appelle la fonction originale func.
  4. Le décorateur retourne le wrapper.

Exemple : Un décorateur qui mesure le temps d'exécution

import time

# 1. La fonction décorateur
def timer_decorator(func):
# 2. La fonction wrapper
# *args et **kwargs permettent au wrapper d'accepter n'importe quels arguments
def wrapper(*args, **kwargs):
start_time = time.time()
# 3. Appel de la fonction originale
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function '{func.__name__}' took {end_time - start_time:.4f} seconds to run.")
return result
# 4. Le décorateur retourne le wrapper
return wrapper

# Utilisation du décorateur
@timer_decorator
def long_running_function(n):
"""Une fonction qui prend un peu de temps."""
total = 0
for i in range(n):
total += i
return total

# Quand on appelle long_running_function, c'est en fait le wrapper qui est appelé.
result = long_running_function(10000000)
# Affiche: Function 'long_running_function' took 0.3456 seconds to run.

Préserver les métadonnées de la fonction : functools.wraps

Un problème avec l'exemple ci-dessus est que le décorateur modifie les métadonnées de la fonction originale.

print(long_running_function.__name__) # Affiche 'wrapper', pas 'long_running_function'

Pour corriger cela, on utilise le décorateur @functools.wraps sur la fonction wrapper. Il copie les métadonnées (nom, docstring, etc.) de la fonction originale sur le wrapper.

Version améliorée du décorateur timer :

import time
import functools

def timer_decorator(func):
@functools.wraps(func) # On décore le wrapper
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function '{func.__name__}' took {end_time - start_time:.4f} seconds.")
return result
return wrapper

@timer_decorator
def another_function():
"""This is a docstring."""
pass

print(another_function.__name__) # Affiche 'another_function'
print(another_function.__doc__) # Affiche 'This is a docstring.'

4. Décorateurs avec arguments

Parfois, on veut pouvoir configurer un décorateur. Pour cela, on doit ajouter un niveau de fonction supplémentaire.

Exemple : Un décorateur qui répète une fonction n fois

import functools

# 1. La fonction externe prend les arguments du décorateur
def repeat(num_times):
# 2. Elle retourne le décorateur lui-même
def decorator_repeat(func):
@functools.wraps(func)
# 3. Le wrapper a accès à num_times
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat

# Utilisation du décorateur avec un argument
@repeat(num_times=3)
def greet(name):
print(f"Hello, {name}!")

greet("Alice")
# Affiche:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

5. Décorateurs de classe

Il est aussi possible de créer des décorateurs en utilisant des classes (en implémentant la méthode __call__), mais les décorateurs basés sur des fonctions sont plus courants et souvent plus simples à comprendre pour commencer. Les décorateurs que nous avons déjà vus (@property, @classmethod, @staticmethod) sont des exemples de décorateurs intégrés au langage.


Exercice 01 : Décorateur de Journalisation (Logging)

Objectif

Cet exercice a pour but de vous faire créer un décorateur simple qui journalise (log) les informations sur l'appel d'une fonction : son nom, les arguments avec lesquels elle a été appelée, et la valeur qu'elle a retournée.

Contexte

Le logging est un cas d'usage parfait pour les décorateurs. Vous voulez souvent savoir quand une fonction est appelée et avec quels paramètres, surtout lors du débogage. Au lieu d'ajouter des print() dans chaque fonction, vous pouvez simplement la décorer.

Énoncé

  1. Créez un nouveau fichier Python nommé logger_decorator.py.

  2. Importez le module functools.

  3. Définissez votre décorateur nommé log_function_call.

    • Il doit accepter une fonction func comme argument.
    • À l'intérieur, définissez une fonction wrapper qui accepte *args et **kwargs pour être compatible avec n'importe quelle fonction.
    • N'oubliez pas de décorer votre wrapper avec @functools.wraps(func) pour préserver les métadonnées de la fonction originale.
  4. Implémentez la logique du wrapper : a. Avant d'appeler la fonction originale, affichez un message indiquant que la fonction est sur le point d'être appelée, incluant son nom (func.__name__), ses arguments positionnels (args) et ses arguments nommés (kwargs). b. Appelez la fonction originale func avec *args et **kwargs et stockez le résultat dans une variable result. c. Après l'appel, affichez un message indiquant que la fonction a terminé, et montrez la valeur de retour result. d. Le wrapper doit retourner result.

  5. Le décorateur log_function_call doit retourner la fonction wrapper.

  6. Testez votre décorateur :

    • Créez une fonction simple, par exemple add(a, b), qui retourne la somme de deux nombres.
    • Décorez-la avec @log_function_call.
    • Appelez la fonction décorée add(3, 5) et observez la sortie.
    • Créez une autre fonction, par exemple greet(name, greeting="Hello"), qui retourne une chaîne de salutation.
    • Décorez-la également et appelez-la avec des arguments positionnels et nommés, comme greet("Alice", greeting="Hi").

Résultat Attendu

Calling function 'add' with args=(3, 5) and kwargs={}
Function 'add' returned 8
Result of add(3, 5): 8
---
Calling function 'greet' with args=('Alice',) and kwargs={'greeting': 'Hi'}
Function 'greet' returned 'Hi, Alice!'
Result of greet("Alice", greeting="Hi"): Hi, Alice!
Cliquez ici pour voir un exemple de code de solution
# logger_decorator.py
import functools

def log_function_call(func):
"""A decorator that logs a function's name, arguments, and return value."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Log before the call
print(f"Calling function '{func.__name__}' with args={args} and kwargs={kwargs}")

# Call the original function
result = func(*args, **kwargs)

# Log after the call
print(f"Function '{func.__name__}' returned {result!r}") # Using !r to get the repr() of the result

return result
return wrapper

# --- Testing ---

@log_function_call
def add(a, b):
"""Returns the sum of two numbers."""
return a + b

@log_function_call
def greet(name, greeting="Hello"):
"""Returns a greeting string."""
return f"{greeting}, {name}!"

# Test 1
result1 = add(3, 5)
print(f"Result of add(3, 5): {result1}")

print("---")

# Test 2
result2 = greet("Alice", greeting="Hi")
print(f"Result of greet(\"Alice\", greeting=\"Hi\"): {result2}")