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 :
# 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: 60Cette approche présente plusieurs problèmes :
- Duplication de code : les lignes de journalisation sont répétées dans chaque fonction
- Mélange des préoccupations : le code de journalisation est mélangé à la logique métier
- Difficile à maintenir : si vous voulez changer le format des logs, vous devez mettre à jour chaque fonction
- 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 :
# 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: 60La 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 :
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 :
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 :
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: 50La 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 :
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: greetingSuivons ce qui se passe :
simple_wrapperreçoitsay_hellosous le nomoriginal_func- Il crée une nouvelle fonction
wrapperqui :- Affiche "Before calling the function"
- Appelle
original_func()(qui estsay_hello) - Affiche "After calling the function"
- Renvoie le résultat
simple_wrapperrenvoie la fonctionwrapper- Quand nous appelons
wrapped_hello(), nous appelons en réalitéwrapper, qui appelle la fonction originalesay_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 :
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 :
*argscollecte tous les arguments positionnels dans un tuple**kwargscollecte 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 :
greet = flexible_wrapper(greet)Nous utiliserons la syntaxe @ :
@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 :
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.0La syntaxe @log_call est exactement équivalente à :
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 :
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: 30Quand les décorateurs sont empilés, ils sont appliqués de bas en haut (celui le plus proche de la fonction en premier) :
@timer # Appliqué en second (couche la plus externe)
@log_call # Appliqué en premier (le plus proche de la fonction)
def process_data(items):
passCeci est équivalent à :
process_data = timer(log_call(process_data))Ordre d’application (de bas en haut) :
@log_callenveloppe d’abord la fonction originale@timerenveloppe 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) :
- L’enveloppe
timerdémarre (plus externe, s’exécute en premier) - L’enveloppe
log_calldémarre (enveloppe interne) - La fonction originale s’exécute
- L’enveloppe
log_callse termine - L’enveloppe
timerse 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 :
Flux d’exécution :
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 :
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 IDCe 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
raisesans argument) - Utilise un bloc
try/exceptpour 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 :
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 digitsCe 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 :
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 -10Ce 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 :
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 :
- Évaluer
@log_call→ Résultat :log_calllui-même (l’objet fonction) - Appliquer à
greet:greet = log_call(greet)
B) Fabrique de décorateurs :
À partir de cet exemple :
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 :
- Évaluer
@repeat(3)→ Résultat :repeat(3)est appelé, renvoie une fonction décorateur - 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 :
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 decoratorNiveau 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) :
# Ce que vous écrivez :
@repeat(3)
def greet(name):
print(f"Hello, {name}!")Étape 1 : Python évalue repeat(3)
decorator = repeat(3) # La fabrique renvoie un décorateur (times=3 est capturé)Étape 2 : Python applique le décorateur à greet
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
greet("Alice") # Appelle en réalité wrapper("Alice")
# wrapper utilise 'times' et 'func' capturésPourquoi trois niveaux ?
Chaque niveau capture des informations différentes via des fermetures :
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 :
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.0Notez 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 :
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 0Quand 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.