Python & AI Tutorials Logo
Programmation Python

38. Décorateurs : ajouter des comportements aux fonctions

Les décorateurs (decorators) font partie des fonctionnalités les plus puissantes de Python pour écrire du code propre et réutilisable. Ils vous permettent de modifier ou d’améliorer le comportement des fonctions sans changer leur code proprement dit. Dans ce chapitre, nous allons nous appuyer sur votre compréhension des fonctions de première classe et des fermetures (closures) du Chapitre 23 pour explorer le fonctionnement des décorateurs et apprendre à les utiliser efficacement.

38.1) Ce que sont les décorateurs et pourquoi ils sont utiles

Un décorateur est une fonction qui prend une autre fonction en entrée et renvoie une version modifiée de cette fonction. Cela est possible parce que, comme nous l’avons vu au Chapitre 23, les fonctions en Python sont des objets de première classe : elles peuvent être passées en arguments et renvoyées par d’autres fonctions. Les décorateurs vous permettent « d’envelopper » un comportement supplémentaire autour de fonctions existantes, ce qui facilite l’ajout de fonctionnalités courantes comme la journalisation, le chronométrage, la validation ou le contrôle d’accès sans encombrer votre logique principale.

Pourquoi les décorateurs comptent

Imaginez que vous avez plusieurs fonctions dans votre programme, et que vous voulez enregistrer quand chacune est appelée. Sans décorateurs, vous pourriez écrire quelque chose comme ceci :

python
# Sans décorateurs - code de journalisation dupliqué
def calculate_total(prices):
    print("Calling calculate_total")
    result = sum(prices)
    print(f"calculate_total returned: {result}")
    return result
 
def find_average(numbers):
    print("Calling find_average")
    result = sum(numbers) / len(numbers)
    print(f"find_average returned: {result}")
    return result
 
def process_order(order_id):
    print("Calling process_order")
    result = f"Order {order_id} processed"
    print(f"process_order returned: {result}")
    return result
 
# Utilisation des fonctions
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60

Cette approche présente plusieurs problèmes :

  1. Duplication de code : les lignes de journalisation sont répétées dans chaque fonction
  2. Mélange des préoccupations : le code de journalisation est mélangé à la logique métier
  3. Difficile à maintenir : si vous voulez changer le format des logs, vous devez mettre à jour chaque fonction
  4. Facile à oublier : les nouvelles fonctions peuvent ne pas inclure la journalisation

Les décorateurs résolvent ces problèmes en vous permettant de séparer le comportement de journalisation de vos fonctions principales :

python
# Avec des décorateurs - propre et facile à maintenir
# (Nous apprendrons à créer @log_calls dans ce chapitre)
 
@log_calls
def calculate_total(prices):
    return sum(prices)
 
@log_calls
def find_average(numbers):
    return sum(numbers) / len(numbers)
 
@log_calls
def process_order(order_id):
    return f"Order {order_id} processed"
 
# L'utilisation des fonctions produit la même sortie
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60

La différence ? Le comportement de journalisation est défini une seule fois dans le décorateur @log_calls et réutilisé partout. Vos fonctions principales restent propres et concentrées sur leur objectif principal.

Cas d’usage courants des décorateurs

Les décorateurs sont particulièrement utiles pour :

  • Journalisation : enregistrer quand les fonctions sont appelées et ce qu’elles renvoient
  • Chronométrage : mesurer combien de temps les fonctions mettent à s’exécuter
  • Validation : vérifier que les arguments d’une fonction respectent certaines exigences
  • Mise en cache : stocker les résultats d’appels coûteux pour les réutiliser
  • Contrôle d’accès : vérifier des permissions avant d’autoriser l’exécution
  • Logique de nouvelle tentative : retenter automatiquement des opérations échouées
  • Vérification de types : valider les types des arguments et des valeurs de retour

L’avantage clé est que vous écrivez le décorateur une seule fois et pouvez l’appliquer à de nombreuses fonctions avec une seule ligne de code.

38.2) Les fonctions comme objets : la base des décorateurs

Avant de pouvoir comprendre les décorateurs, nous devons revoir et approfondir le concept selon lequel les fonctions sont des objets de première classe en Python. Comme nous l’avons vu au Chapitre 23, cela signifie que les fonctions peuvent être affectées à des variables, passées en arguments et renvoyées par d’autres fonctions.

Les fonctions peuvent être affectées à des variables

Quand vous définissez une fonction, Python crée un objet fonction et l’associe à un nom :

python
def greet(name):
    return f"Hello, {name}!"
 
# L'objet fonction peut être affecté à une autre variable
say_hello = greet
 
# Les deux noms font référence au même objet fonction
print(greet("Alice"))      # Output: Hello, Alice!
print(say_hello("Bob"))    # Output: Hello, Bob!

Les noms greet et say_hello font tous deux référence au même objet fonction. C’est fondamental pour comprendre comment fonctionnent les décorateurs.

Les fonctions peuvent être passées en arguments

Vous pouvez passer des fonctions à d’autres fonctions comme n’importe quelle autre valeur :

python
def apply_twice(func, value):
    """Apply a function to a value twice."""
    result = func(value)
    result = func(result)
    return result
 
def add_five(x):
    return x + 5
 
result = apply_twice(add_five, 10)
print(result)  # Output: 20 (10 + 5 = 15, then 15 + 5 = 20)

Ici, apply_twice reçoit la fonction add_five en argument et l’appelle deux fois.

Les fonctions peuvent renvoyer d’autres fonctions

Une fonction peut créer et renvoyer une nouvelle fonction :

python
def make_multiplier(factor):
    """Create a function that multiplies by a specific factor."""
    def multiply(x):
        return x * factor
    return multiply
 
times_three = make_multiplier(3)
times_five = make_multiplier(5)
 
print(times_three(10))  # Output: 30
print(times_five(10))   # Output: 50

La fonction make_multiplier renvoie une nouvelle fonction qui « se souvient » de la valeur factor via une fermeture (comme nous l’avons vu au Chapitre 23).

Envelopper des fonctions : le motif de base des décorateurs

Le motif de décorateur combine ces concepts : une fonction qui prend une fonction en entrée, crée une fonction wrapper(enveloppe) qui ajoute un comportement, et renvoie cette enveloppe :

python
def simple_wrapper(original_func):
    """Wrap a function with additional behavior."""
    def wrapper():
        print("Before calling the function")
        result = original_func()
        print("After calling the function")
        return result
    return wrapper
 
def say_hello():
    print("Hello!")
    return "greeting"
 
# Manually wrap the function
wrapped_hello = simple_wrapper(say_hello)
return_value = wrapped_hello()
# Output:
# Before calling the function
# Hello!
# After calling the function
 
print(f"Returned: {return_value}")
# Output: Returned: greeting

Suivons ce qui se passe :

  1. simple_wrapper reçoit say_hello sous le nom original_func
  2. Il crée une nouvelle fonction wrapper qui :
    • Affiche "Before calling the function"
    • Appelle original_func() (qui est say_hello)
    • Affiche "After calling the function"
    • Renvoie le résultat
  3. simple_wrapper renvoie la fonction wrapper
  4. Quand nous appelons wrapped_hello(), nous appelons en réalité wrapper, qui appelle la fonction originale say_hello à l’intérieur

C’est le motif de base derrière tous les décorateurs.

Gérer des fonctions avec des arguments

L’enveloppe ci-dessus ne fonctionne qu’avec les fonctions qui ne prennent aucun argument. Pour qu’elle fonctionne avec n’importe quelle fonction, nous avons besoin de *args et **kwargs :

python
def flexible_wrapper(original_func):
    """Wrap a function that can accept any arguments."""
    def wrapper(*args, **kwargs):
        # *args captures positional arguments
        # **kwargs captures keyword arguments
        print("Before calling the function")
        result = original_func(*args, **kwargs)
        print("After calling the function")
        return result
    return wrapper
 
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"
 
# Manually wrap the function
greet = flexible_wrapper(greet)
 
result = greet("Alice")
# Output:
# Before calling the function
# After calling the function
 
print(result)
# Output: Hello, Alice!
 
result = greet("Bob", greeting="Hi")
# Output:
# Before calling the function
# After calling the function
 
print(result)
# Output: Hi, Bob!

Comment fonctionnent *args et **kwargs :

Comme nous l’avons vu au Chapitre 20, *args et **kwargs permettent aux fonctions d’accepter un nombre variable d’arguments :

  • *args collecte tous les arguments positionnels dans un tuple
  • **kwargs collecte tous les arguments nommés dans un dictionnaire
  • Quand nous appelons original_func(*args, **kwargs), nous les dépaquetons à nouveau en tant qu’arguments de la fonction originale

Ce motif permet à notre enveloppe de fonctionner avec n’importe quelle fonction, quel que soit le nombre d’arguments qu’elle prend.

Passer à une syntaxe plus propre

Ce motif est la base des décorateurs. La syntaxe des décorateurs que nous allons apprendre ensuite n’est qu’une façon plus propre d’appliquer ce motif. Au lieu d’écrire :

python
greet = flexible_wrapper(greet)

Nous utiliserons la syntaxe @ :

python
@flexible_wrapper
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

Les deux font exactement la même chose : la syntaxe @ n’est que du sucre syntaxique qui rend le code plus propre et plus lisible.

38.3) La syntaxe @decorator : une application plus propre

Écrire function_name = decorator(function_name) fonctionne, mais c’est verbeux et facile à oublier. Python fournit la syntaxe @decorator comme une façon plus propre d’appliquer des décorateurs.

Utiliser le symbole @

Au lieu d’envelopper manuellement une fonction, vous pouvez placer @decorator_name sur la ligne immédiatement avant la définition de la fonction :

python
def log_call(func):
    """Decorator that logs function calls."""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
@log_call
def calculate_total(prices):
    return sum(prices)
 
@log_call
def find_average(numbers):
    return sum(numbers) / len(numbers)
 
# Use the decorated functions
total = calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60
 
print(f"Total: {total}")
# Output: Total: 60
 
average = find_average([10, 20, 30])
# Output:
# Calling find_average
# find_average returned: 20.0
 
print(f"Average: {average}")
# Output: Average: 20.0

La syntaxe @log_call est exactement équivalente à :

python
def calculate_total(prices):
    return sum(prices)
 
calculate_total = log_call(calculate_total)

Mais la syntaxe @ est beaucoup plus propre et rend immédiatement évident que la fonction est décorée.

Empiler plusieurs décorateurs

Vous pouvez appliquer plusieurs décorateurs à la même fonction en les empilant :

python
import time
 
def log_call(func):
    """Decorator that logs function calls."""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
def timer(func):
    """Decorator that times function execution."""
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start_time
        print(f"{func.__name__} took {elapsed:.4f} seconds")
        return result
    return wrapper
 
@timer
@log_call
def process_data(items):
    total = sum(items)
    return total * 2
 
result = process_data([1, 2, 3, 4, 5])
# Output:
# Calling process_data
# process_data returned: 30
# process_data took 0.0001 seconds
 
print(f"Final result: {result}")
# Output: Final result: 30

Quand les décorateurs sont empilés, ils sont appliqués de bas en haut (celui le plus proche de la fonction en premier) :

python
@timer          # Appliqué en second (couche la plus externe)
@log_call       # Appliqué en premier (le plus proche de la fonction)
def process_data(items):
    pass

Ceci est équivalent à :

python
process_data = timer(log_call(process_data))

Ordre d’application (de bas en haut) :

  1. @log_call enveloppe d’abord la fonction originale
  2. @timer enveloppe le résultat (enveloppe la fonction déjà enveloppée)

Ordre d’exécution (de haut en bas, de la couche la plus externe vers la plus interne) :

  1. L’enveloppe timer démarre (plus externe, s’exécute en premier)
  2. L’enveloppe log_call démarre (enveloppe interne)
  3. La fonction originale s’exécute
  4. L’enveloppe log_call se termine
  5. L’enveloppe timer se termine (plus externe, se termine en dernier)

Considérez les décorateurs comme des couches de papier cadeau : vous les appliquez de l’intérieur vers l’extérieur, mais quand vous déballez (exécutez), vous allez de l’extérieur vers l’intérieur.

Application du décorateur :

Fonction originale
process_data

Étape 1 : @log_call(décorateur du bas)

log_call enveloppe l’originale

Étape 2 : @timer(décorateur du haut)

timer enveloppe l’enveloppe log_call

Final : timer enveloppe log_call qui enveloppe l’originale

Flux d’exécution :

Appel de process_data

1. l’enveloppe timer démarre
2. l’enveloppe log_call démarre
3. la fonction originale s’exécute
4. l’enveloppe log_call se termine
5. l’enveloppe timer se termine

Renvoie le résultat

38.4) Exemples pratiques de décorateurs (journalisation, chronométrage, validation)

Explorons maintenant plusieurs décorateurs pratiques que vous pourriez utiliser dans de vrais programmes. Ces exemples démontrent des motifs courants et montrent comment les décorateurs résolvent des problèmes du monde réel.

Exemple 1 : Décorateur de journalisation amélioré

Un décorateur de journalisation plus sophistiqué qui inclut des horodatages et gère les exceptions :

python
import time
 
def log_with_timestamp(func):
    """Decorator that logs function calls with timestamps."""
    def wrapper(*args, **kwargs):
        timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{timestamp}] Calling {func.__name__}")
        
        try:
            result = func(*args, **kwargs)
            print(f"[{timestamp}] {func.__name__} completed successfully")
            return result
        except Exception as e:
            print(f"[{timestamp}] {func.__name__} raised {type(e).__name__}: {e}")
            raise
    
    return wrapper
 
@log_with_timestamp
def divide(a, b):
    return a / b
 
@log_with_timestamp
def process_user(user_id):
    # Simuler le traitement
    if user_id < 0:
        raise ValueError("User ID must be positive")
    return f"Processed user {user_id}"
 
# Tester une exécution réussie
result = divide(10, 2)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide completed successfully
 
print(f"Result: {result}")
# Output: Result: 5.0
 
# Tester une exécution réussie avec validation
user = process_user(42)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user completed successfully
 
print(user)
# Output: Processed user 42
 
# Tester la gestion des exceptions
try:
    divide(10, 0)
    # Output:
    # [2025-12-31 10:30:45] Calling divide
    # [2025-12-31 10:30:45] divide raised ZeroDivisionError: division by zero
except ZeroDivisionError:
    print("Handled division by zero")
    # Output: Handled division by zero
 
try:
    process_user(-5)
    # Output:
    # [2025-12-31 10:30:45] Calling process_user
    # [2025-12-31 10:30:45] process_user raised ValueError: User ID must be positive
except ValueError:
    print("Handled invalid user ID")
    # Output: Handled invalid user ID

Ce décorateur :

  • Ajoute des horodatages à tous les messages de log
  • Journalise à la fois les fins réussies et les exceptions
  • Relance les exceptions après les avoir journalisées (en utilisant raise sans argument)
  • Utilise un bloc try/except pour intercepter et journaliser toute exception

Exemple 2 : Décorateur de chronométrage des performances

Un décorateur qui mesure et rapporte le temps d’exécution d’une fonction :

python
import time
 
def measure_time(func):
    """Decorator that measures and reports execution time."""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        
        # Formater le temps de manière appropriée
        if elapsed < 0.001:
            time_str = f"{elapsed * 1000000:.2f} microseconds"
        elif elapsed < 1:
            time_str = f"{elapsed * 1000:.2f} milliseconds"
        else:
            time_str = f"{elapsed:.2f} seconds"
        
        print(f"{func.__name__} executed in {time_str}")
        return result
    
    return wrapper
 
@measure_time
def find_primes(limit):
    """Find all prime numbers up to limit."""
    primes = []
    for num in range(2, limit):
        is_prime = True
        for divisor in range(2, int(num ** 0.5) + 1):
            if num % divisor == 0:
                is_prime = False
                break
        if is_prime:
            primes.append(num)
    return primes
 
@measure_time
def calculate_factorial(n):
    """Calculate factorial of n."""
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result
 
# Tester les fonctions décorées
primes = find_primes(1000)
# Output: find_primes executed in 15.23 milliseconds
 
print(f"Found {len(primes)} primes")
# Output: Found 168 primes
 
factorial = calculate_factorial(100)
# Output: calculate_factorial executed in 45.67 microseconds
 
print(f"Factorial has {len(str(factorial))} digits")
# Output: Factorial has 158 digits

Ce décorateur formate automatiquement la mesure du temps de manière appropriée (microsecondes, millisecondes ou secondes) selon la durée.

Exemple 3 : Décorateur de validation des entrées

Un décorateur qui valide les arguments d’une fonction avant l’exécution :

python
def validate_positive(func):
    """Decorator that ensures all numeric arguments are positive."""
    def wrapper(*args, **kwargs):
        # Vérifier les arguments positionnels
        for i, arg in enumerate(args):
            if isinstance(arg, (int, float)) and arg <= 0:
                raise ValueError(
                    f"Argument {i} to {func.__name__} must be positive, got {arg}"
                )
        
        # Vérifier les arguments nommés
        for key, value in kwargs.items():
            if isinstance(value, (int, float)) and value <= 0:
                raise ValueError(
                    f"Argument '{key}' to {func.__name__} must be positive, got {value}"
                )
        
        return func(*args, **kwargs)
    
    return wrapper
 
@validate_positive
def calculate_area(width, height):
    """Calculate area of a rectangle."""
    return width * height
 
@validate_positive
def calculate_discount(price, discount_percent):
    """Calculate discounted price."""
    discount = price * (discount_percent / 100)
    return price - discount
 
# Tester des entrées valides
area = calculate_area(10, 5)
print(f"Area: {area}")
# Output: Area: 50
 
discounted = calculate_discount(100, 20)
print(f"Discounted price: ${discounted:.2f}")
# Output: Discounted price: $80.00
 
# Tester des entrées invalides
try:
    calculate_area(-5, 10)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: Argument 0 to calculate_area must be positive, got -5
 
try:
    calculate_discount(100, discount_percent=-10)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: Argument 'discount_percent' to calculate_discount must be positive, got -10

Ce décorateur :

  • Vérifie tous les arguments numériques (à la fois positionnels et nommés)
  • Lève une erreur descriptive si l’un d’eux n’est pas positif
  • Fournit des messages d’erreur clairs indiquant quel argument a échoué la validation

38.5) (Optionnel) Décorateurs avec arguments

Jusqu’ici, tous nos décorateurs ont été de simples fonctions qui prennent une fonction en entrée. Mais que se passe-t-il si vous voulez configurer le comportement d’un décorateur ? Par exemple, vous pourriez vouloir un décorateur de nouvelle tentative où vous pouvez spécifier le nombre d’essais, ou un décorateur de journalisation où vous pouvez spécifier le niveau de log.

Les décorateurs avec arguments nécessitent un niveau supplémentaire d’imbrication de fonctions. Au lieu qu’un décorateur soit une fonction qui prend une fonction, il devient une fonction qui prend des arguments et renvoie un décorateur.

Le motif : les fabriques de décorateurs

Un décorateur avec arguments est en fait une fabrique de décorateurs(decorator factory) : une fonction qui crée et renvoie un décorateur. La clé pour comprendre cela est de savoir ce que fait Python avec le symbole @.

Le principe clé : Python évalue d’abord @

Python évalue toujours ce qui vient après @ en premier, puis utilise le résultat pour décorer votre fonction.

Comparons :

A) Décorateur de base :

À partir de cet exemple :

python
def log_call(func):
    """Decorator that logs function calls."""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
@log_call
def greet(name):
    return f"Hello, {name}!"

Ce que Python fait :

  1. Évaluer @log_call → Résultat : log_call lui-même (l’objet fonction)
  2. Appliquer à greet : greet = log_call(greet)

B) Fabrique de décorateurs :

À partir de cet exemple :

python
def repeat(times):
    """Level 1: Factory - receives configuration"""
    def decorator(func):
        """Level 2: Decorator - receives the function to decorate"""
        def wrapper(*args, **kwargs):
            """Level 3: Wrapper - executes when decorated function is called"""
            for i in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator
 
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")
 
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

Ce que Python fait :

  1. Évaluer @repeat(3) → Résultat : repeat(3) est appelé, renvoie une fonction décorateur
  2. Appliquer ce décorateur à greet : greet = decorator(greet)

La différence : @log_call vous donne la fonction elle-même, mais @repeat(3) appelle une fonction (repeat) qui renvoie un décorateur.

Comprendre les trois niveaux

Une fabrique de décorateurs a trois fonctions imbriquées, chacune avec un rôle spécifique :

python
def repeat(times):                      # Level 1: Factory
    def decorator(func):                # Level 2: Decorator  
        def wrapper(*args, **kwargs):   # Level 3: Wrapper
            for i in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

Niveau 1 - Fabrique (repeat) :

  • Prend : la configuration (times)
  • Renvoie : une fonction décorateur
  • Appelée : quand Python évalue @repeat(3)

Niveau 2 - Décorateur (decorator) :

  • Prend : la fonction à décorer (func)
  • Renvoie : une fonction wrapper(enveloppe)
  • Appelé : immédiatement après le Niveau 1, dans le cadre de la syntaxe @

Niveau 3 - Wrapper (wrapper) :

  • Prend : les arguments de la fonction lorsqu’elle est appelée (*args, **kwargs)
  • Renvoie : le résultat
  • Appelé : à chaque fois que vous appelez la fonction décorée

Exécution étape par étape

Suivons ce qui se passe avec @repeat(3) :

python
# Ce que vous écrivez :
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

Étape 1 : Python évalue repeat(3)

python
decorator = repeat(3)  # La fabrique renvoie un décorateur (times=3 est capturé)

Étape 2 : Python applique le décorateur à greet

python
def greet(name):
    print(f"Hello, {name}!")
 
greet = decorator(greet)  # Le décorateur renvoie une enveloppe (func=greet est capturé)

Note : À ce stade, greet fait maintenant référence à la fonction wrapper. La fonction greet originale est capturée dans func.

Étape 3 : Quand vous appelez greet("Alice"), l’enveloppe s’exécute

python
greet("Alice")  # Appelle en réalité wrapper("Alice")
# wrapper utilise 'times' et 'func' capturés

Pourquoi trois niveaux ?

Chaque niveau capture des informations différentes via des fermetures :

python
def repeat(times):                      # Capture : times
    def decorator(func):                # Capture : func (et se souvient de times)
        def wrapper(*args, **kwargs):   # Capture : times, func, et reçoit args
            for i in range(times):      # Utilise 'times' capturé
                result = func(*args, **kwargs)  # Utilise 'func' et 'args' capturés
            return result
        return wrapper
    return decorator
  • Niveau 1 capture la configuration (times)
  • Niveau 2 capture la fonction à décorer (func)
  • Niveau 3 reçoit les arguments lors de l’appel (args, kwargs)

Sans ces trois niveaux, nous ne pourrions pas avoir un décorateur configurable qui se souvient à la fois de ses paramètres et de la fonction qu’il décore.

Exemple 1 : Un décorateur de journalisation configurable

Voici un exemple pratique d’un décorateur de journalisation qui accepte une configuration :

python
def log_with_prefix(prefix="LOG"):
    """Decorator factory that creates a logging decorator with a custom prefix."""
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{prefix}] Calling {func.__name__}")
            result = func(*args, **kwargs)
            print(f"[{prefix}] {func.__name__} returned: {result}")
            return result
        return wrapper
    return decorator
 
@log_with_prefix(prefix="INFO")
def calculate_total(prices):
    return sum(prices)
 
@log_with_prefix()  # Use default prefix
def get_average(numbers):
    return sum(numbers) / len(numbers)
 
# Test the decorated functions
total = calculate_total([10, 20, 30])
# Output:
# [INFO] Calling calculate_total
# [INFO] calculate_total returned: 60
 
print(f"Total: {total}")
# Output: Total: 60
 
average = get_average([10, 20, 30])
# Output:
# [LOG] Calling get_average
# [LOG] get_average returned: 20.0
 
print(f"Average: {average}")
# Output: Average: 20.0

Notez que :

  • @log_with_prefix(prefix="INFO") utilise un préfixe personnalisé
  • @log_with_prefix() utilise le préfixe par défaut "LOG"
  • Vous devez inclure des parenthèses même quand vous utilisez les valeurs par défaut

Exemple 2 : Un décorateur avec plusieurs arguments

Voici un décorateur qui valide des plages numériques :

python
def validate_range(min_value=None, max_value=None):
    """
    Decorator factory that validates numeric arguments are within a range.
    
    Args:
        min_value: Minimum allowed value (inclusive)
        max_value: Maximum allowed value (inclusive)
    """
    def decorator(func):
        def wrapper(*args, **kwargs):
            # Check all numeric arguments
            all_args = list(args) + list(kwargs.values())
            
            for arg in all_args:
                if isinstance(arg, (int, float)):
                    if min_value is not None and arg < min_value:
                        raise ValueError(
                            f"{func.__name__} received {arg}, "
                            f"which is below minimum {min_value}"
                        )
                    if max_value is not None and arg > max_value:
                        raise ValueError(
                            f"{func.__name__} received {arg}, "
                            f"which is above maximum {max_value}"
                        )
            
            return func(*args, **kwargs)
        return wrapper
    return decorator
 
@validate_range(min_value=0, max_value=100)
def calculate_percentage(value, total):
    """Calculate percentage."""
    return (value / total) * 100
 
@validate_range(min_value=0)
def calculate_age(birth_year, current_year):
    """Calculate age from birth year."""
    return current_year - birth_year
 
# Test valid inputs
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")
# Output: Percentage: 25.0%
 
age = calculate_age(1990, 2025)
print(f"Age: {age}")
# Output: Age: 35
 
# Test invalid inputs
try:
    calculate_percentage(150, 100)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: calculate_percentage received 150, which is above maximum 100
 
try:
    calculate_age(-5, 2025)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: calculate_age received -5, which is below minimum 0

Quand utiliser des décorateurs avec arguments

Utilisez des décorateurs avec arguments quand :

  • Vous devez configurer le comportement du décorateur
  • Le même décorateur doit se comporter différemment selon le contexte
  • Vous voulez rendre les décorateurs plus réutilisables et flexibles

Exemples courants :

  • Décorateurs de nouvelle tentative avec nombre de tentatives et délais configurables
  • Décorateurs de journalisation avec niveaux ou formats configurables
  • Décorateurs de validation avec règles configurables
  • Décorateurs de mise en cache avec tailles de cache ou durées d’expiration configurables
  • Limitation de débit(rate limiting) avec limites configurables

Une note sur la complexité

Les décorateurs avec arguments ajoutent un niveau de complexité supplémentaire. Quand vous les écrivez :

  • Utilisez des noms de paramètres clairs et descriptifs
  • Fournissez des valeurs par défaut raisonnables
  • Incluez des docstrings expliquant les paramètres
  • Demandez-vous si la flexibilité ajoutée vaut la complexité

Pour les cas simples, un décorateur sans arguments est souvent plus clair et plus facile à comprendre.


Les décorateurs sont un outil puissant pour écrire du code Python propre et maintenable. Ils vous permettent de séparer des préoccupations transversales (comme la journalisation, le chronométrage et la validation) de votre logique métier principale, ce qui rend votre code plus facile à lire, à tester et à modifier. À mesure que vous continuerez à programmer en Python, vous verrez des décorateurs utilisés abondamment dans les frameworks et les bibliothèques, et vous découvrirez de nombreuses occasions d’écrire vos propres décorateurs pour résoudre élégamment des problèmes courants.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai