38. Decoratori: aggiungere comportamento alle funzioni
I decoratori (decorators) sono una delle funzionalità più potenti di Python per scrivere codice pulito e riutilizzabile. Ti permettono di modificare o migliorare il comportamento delle funzioni senza cambiarne il codice effettivo. In questo capitolo, faremo leva sulla tua comprensione delle funzioni di prima classe e delle chiusure (closures) del Capitolo 23 per esplorare come funzionano i decoratori e come usarli in modo efficace.
38.1) Cosa sono i decoratori e perché sono utili
Un decoratore è una funzione che prende un'altra funzione in input e restituisce una versione modificata di quella funzione. Questo è possibile perché, come abbiamo visto nel Capitolo 23, le funzioni in Python sono oggetti di prima classe: possono essere passate come argomenti e restituite da altre funzioni. I decoratori ti consentono di “avvolgere” un comportamento aggiuntivo attorno a funzioni esistenti, rendendo semplice aggiungere funzionalità comuni come logging, misurazione dei tempi, validazione o controllo degli accessi senza appesantire la logica principale.
Perché i decoratori sono importanti
Immagina di avere diverse funzioni nel tuo programma e di voler registrare quando ciascuna viene chiamata. Senza decoratori, potresti scrivere qualcosa del genere:
# Senza decoratori - codice di logging duplicato
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
# Usare le funzioni
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60Questo approccio presenta diversi problemi:
- Duplicazione del codice: le righe di logging sono ripetute in ogni funzione.
- Mescolare responsabilità: il codice di logging è mescolato con la logica di business.
- Difficile da mantenere: se vuoi cambiare il formato del logging, devi aggiornare ogni funzione.
- Facile da dimenticare: le nuove funzioni potrebbero non includere il logging.
I decoratori risolvono questi problemi permettendoti di separare il comportamento di logging dalle tue funzioni principali:
# Con i decoratori - pulito e manutenibile
# (Impareremo come creare @log_calls in questo capitolo)
@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"
# Usare le funzioni produce lo stesso output
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60La differenza? Il comportamento di logging è definito una sola volta nel decoratore @log_calls e riutilizzato ovunque. Le tue funzioni principali restano pulite e focalizzate sul loro scopo primario.
Casi d’uso comuni dei decoratori
I decoratori sono particolarmente utili per:
- Logging: registrare quando le funzioni vengono chiamate e cosa restituiscono.
- Timing: misurare quanto tempo impiegano le funzioni a eseguire.
- Validation: verificare che gli argomenti delle funzioni rispettino determinati requisiti.
- Caching: memorizzare i risultati di chiamate di funzione costose per riutilizzarli.
- Access control: controllare i permessi prima di consentire l’esecuzione di una funzione.
- Retry logic: ritentare automaticamente operazioni fallite.
- Type checking: validare i tipi degli argomenti e del valore di ritorno.
Il vantaggio principale è che scrivi il decoratore una sola volta e puoi applicarlo a molte funzioni con una singola riga di codice.
38.2) Le funzioni come oggetti: le fondamenta dei decoratori
Prima di poter comprendere i decoratori, dobbiamo ripassare ed estendere il concetto che le funzioni sono oggetti di prima classe in Python. Come abbiamo visto nel Capitolo 23, questo significa che le funzioni possono essere assegnate a variabili, passate come argomenti e restituite da altre funzioni.
Le funzioni possono essere assegnate a variabili
Quando definisci una funzione, Python crea un oggetto funzione e lo associa a un nome:
def greet(name):
return f"Hello, {name}!"
# L'oggetto funzione può essere assegnato a un'altra variabile
say_hello = greet
# Entrambi i nomi si riferiscono allo stesso oggetto funzione
print(greet("Alice")) # Output: Hello, Alice!
print(say_hello("Bob")) # Output: Hello, Bob!I nomi greet e say_hello si riferiscono entrambi allo stesso oggetto funzione. Questo è fondamentale per capire come funzionano i decoratori.
Le funzioni possono essere passate come argomenti
Puoi passare le funzioni ad altre funzioni proprio come qualsiasi altro valore:
def apply_twice(func, value):
"""Applica una funzione a un valore due volte."""
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, poi 15 + 5 = 20)Qui, apply_twice riceve la funzione add_five come argomento e la chiama due volte.
Le funzioni possono restituire altre funzioni
Una funzione può creare e restituire una nuova funzione:
def make_multiplier(factor):
"""Crea una funzione che moltiplica per un fattore specifico."""
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 funzione make_multiplier restituisce una nuova funzione che “ricorda” il valore factor tramite una chiusura (closure) (come abbiamo visto nel Capitolo 23).
Wrapping delle funzioni: il pattern di base dei decoratori
Il pattern dei decoratori combina questi concetti: una funzione che prende una funzione in input, crea una funzione wrapper che aggiunge comportamento e restituisce il wrapper:
def simple_wrapper(original_func):
"""Avvolge una funzione con comportamento aggiuntivo."""
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"
# Avvolgere manualmente la funzione
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: greetingTracciamo cosa succede:
simple_wrapperricevesay_hellocomeoriginal_func- Crea una nuova funzione
wrapperche:- Stampa "Before calling the function"
- Chiama
original_func()(che èsay_hello) - Stampa "After calling the function"
- Restituisce il risultato
simple_wrapperrestituisce la funzionewrapper- Quando chiamiamo
wrapped_hello(), in realtà stiamo chiamandowrapper, che chiama l’originalesay_helloal suo interno
Questo è il pattern fondamentale alla base di tutti i decoratori.
Gestire funzioni con argomenti
Il wrapper sopra funziona solo con funzioni che non accettano argomenti. Per farlo funzionare con qualsiasi funzione, ci servono *args e **kwargs:
def flexible_wrapper(original_func):
"""Avvolge una funzione che può accettare qualsiasi argomento."""
def wrapper(*args, **kwargs):
# *args cattura gli argomenti posizionali
# **kwargs cattura gli argomenti con nome (keyword)
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}!"
# Avvolgere manualmente la funzione
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!Come funzionano *args e **kwargs:
Come abbiamo visto nel Capitolo 20, *args e **kwargs permettono alle funzioni di accettare un numero variabile di argomenti:
*argsraccoglie tutti gli argomenti posizionali in una tupla**kwargsraccoglie tutti gli argomenti con nome in un dizionario- Quando chiamiamo
original_func(*args, **kwargs), li “spacchettiamo” di nuovo come argomenti per la funzione originale
Questo pattern consente al nostro wrapper di funzionare con qualsiasi funzione, indipendentemente da quanti argomenti accetta.
Passare a una sintassi più pulita
Questo pattern è la base dei decoratori. La sintassi dei decoratori che impareremo a breve è solo un modo più pulito di applicare questo pattern. Invece di scrivere:
greet = flexible_wrapper(greet)Useremo la sintassi @:
@flexible_wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"Entrambe fanno esattamente la stessa cosa: la sintassi @ è solo zucchero sintattico che rende il codice più pulito e leggibile.
38.3) La sintassi @decorator: applicazione più pulita
Scrivere function_name = decorator(function_name) funziona, ma è prolisso e facile da dimenticare. Python fornisce la sintassi @decorator come modo più pulito per applicare i decoratori.
Usare il simbolo @
Invece di avvolgere manualmente una funzione, puoi mettere @decorator_name sulla riga immediatamente precedente la definizione della funzione:
def log_call(func):
"""Decoratore che registra le chiamate di funzione."""
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)
# Usa le funzioni decorate
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 sintassi @log_call è esattamente equivalente a scrivere:
def calculate_total(prices):
return sum(prices)
calculate_total = log_call(calculate_total)Ma la sintassi @ è molto più pulita e rende immediatamente evidente che la funzione è decorata.
Impilare più decoratori
Puoi applicare più decoratori alla stessa funzione impilandoli:
import time
def log_call(func):
"""Decoratore che registra le chiamate di funzione."""
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):
"""Decoratore che misura il tempo di esecuzione della funzione."""
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: 30Quando i decoratori sono impilati, vengono applicati dal basso verso l’alto (prima quello più vicino alla funzione):
@timer # Applicato per secondo (strato più esterno)
@log_call # Applicato per primo (più vicino alla funzione)
def process_data(items):
passQuesto equivale a:
process_data = timer(log_call(process_data))Ordine di applicazione (dal basso verso l’alto):
@log_callavvolge per primo la funzione originale@timeravvolge il risultato (avvolge la funzione già avvolta)
Ordine di esecuzione (dall’alto verso il basso, dallo strato più esterno a quello più interno):
- parte il wrapper di
timer(più esterno, esegue per primo) - parte il wrapper di
log_call(wrapper interno) - esegue la funzione originale
- termina il wrapper di
log_call - termina il wrapper di
timer(più esterno, termina per ultimo)
Pensa ai decoratori come a strati di carta da regalo: li applichi dall’interno verso l’esterno, ma quando scarti (esegui), vai dall’esterno verso l’interno.
Applicazione dei decoratori:
Flusso di esecuzione:
38.4) Esempi pratici di decoratori (Logging, Timing, Validation)
Ora esploriamo diversi decoratori pratici che potresti usare in programmi reali. Questi esempi dimostrano pattern comuni e mostrano come i decoratori risolvano problemi del mondo reale.
Esempio 1: Decoratore di logging avanzato
Un decoratore di logging più sofisticato che include timestamp e gestisce le eccezioni:
import time
def log_with_timestamp(func):
"""Decoratore che registra le chiamate di funzione con timestamp."""
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):
# Simula l'elaborazione
if user_id < 0:
raise ValueError("User ID must be positive")
return f"Processed user {user_id}"
# Test dell'esecuzione riuscita
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
# Test dell'esecuzione riuscita con validazione
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
# Test della gestione delle eccezioni
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 IDQuesto decoratore:
- Aggiunge timestamp a tutti i messaggi di log
- Registra sia i completamenti riusciti sia le eccezioni
- Rilancia le eccezioni dopo averle registrate (usando
raisesenza argomento) - Usa un blocco
try/exceptper catturare e registrare qualsiasi eccezione
Esempio 2: Decoratore per il timing delle prestazioni
Un decoratore che misura e riporta il tempo di esecuzione di una funzione:
import time
def measure_time(func):
"""Decoratore che misura e riporta il tempo di esecuzione."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
# Formatta il tempo in modo appropriato
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):
"""Trova tutti i numeri primi fino a 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):
"""Calcola il fattoriale di n."""
result = 1
for i in range(1, n + 1):
result *= i
return result
# Test delle funzioni decorate
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 digitsQuesto decoratore formatta automaticamente la misurazione del tempo in modo appropriato (microsecondi, millisecondi o secondi) in base alla durata.
Esempio 3: Decoratore di validazione dell’input
Un decoratore che valida gli argomenti di una funzione prima dell’esecuzione:
def validate_positive(func):
"""Decoratore che garantisce che tutti gli argomenti numerici siano positivi."""
def wrapper(*args, **kwargs):
# Controlla gli argomenti posizionali
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}"
)
# Controlla gli argomenti con nome (keyword)
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):
"""Calcola l'area di un rettangolo."""
return width * height
@validate_positive
def calculate_discount(price, discount_percent):
"""Calcola il prezzo scontato."""
discount = price * (discount_percent / 100)
return price - discount
# Test con input validi
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
# Test con input non validi
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 -10Questo decoratore:
- Controlla tutti gli argomenti numerici (sia posizionali sia con nome)
- Solleva un errore descrittivo se qualcuno non è positivo
- Fornisce messaggi di errore chiari che indicano quale argomento non ha superato la validazione
38.5) (Opzionale) Decoratori con argomenti
Finora, tutti i nostri decoratori sono stati funzioni semplici che prendono una funzione in input. Ma se volessi configurare il comportamento di un decoratore? Per esempio, potresti volere un decoratore di retry in cui specificare il numero di tentativi, oppure un decoratore di logging in cui specificare il livello di log.
I decoratori con argomenti richiedono un livello extra di annidamento delle funzioni. Invece di essere una funzione che prende una funzione, un decoratore diventa una funzione che prende argomenti e restituisce un decoratore.
Il pattern: factory di decoratori
Un decoratore con argomenti è in realtà una factory di decoratori (decorator factory) - una funzione che crea e restituisce un decoratore. La chiave per capirlo è sapere cosa fa Python con il simbolo @.
Il principio chiave: Python valuta prima @
Python valuta sempre prima qualunque cosa venga dopo @, poi usa il risultato per decorare la tua funzione.
Confrontiamo:
A) Decoratore di base:
Basato su questo esempio:
def log_call(func):
"""Decoratore che registra le chiamate di funzione."""
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}!"Cosa fa Python:
- Valuta
@log_call→ Risultato:log_callstesso (l’oggetto funzione) - Applica a
greet:greet = log_call(greet)
B) Factory di decoratori:
Basato su questo esempio:
def repeat(times):
"""Livello 1: Factory - riceve la configurazione"""
def decorator(func):
"""Livello 2: Decoratore - riceve la funzione da decorare"""
def wrapper(*args, **kwargs):
"""Livello 3: Wrapper - esegue quando la funzione decorata viene chiamata"""
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!Cosa fa Python:
- Valuta
@repeat(3)→ Risultato:repeat(3)viene chiamata, e restituisce una funzione decoratore - Applica quel decoratore a
greet:greet = decorator(greet)
La differenza: @log_call ti dà la funzione stessa, ma @repeat(3) chiama una funzione (repeat) che restituisce un decoratore.
Comprendere i tre livelli
Una factory di decoratori ha tre funzioni annidate, ciascuna con un ruolo specifico:
def repeat(times): # Livello 1: Factory
def decorator(func): # Livello 2: Decoratore
def wrapper(*args, **kwargs): # Livello 3: Wrapper
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decoratorLivello 1 - Factory (repeat):
- Prende: configurazione (
times) - Restituisce: una funzione decoratore
- Viene chiamata: quando Python valuta
@repeat(3)
Livello 2 - Decoratore (decorator):
- Prende: la funzione da decorare (
func) - Restituisce: una funzione wrapper
- Viene chiamato: immediatamente dopo il Livello 1, come parte della sintassi @
Livello 3 - Wrapper (wrapper):
- Prende: gli argomenti della funzione quando viene chiamata (
*args, **kwargs) - Restituisce: il risultato
- Viene chiamato: ogni volta che chiami la funzione decorata
Esecuzione passo per passo
Tracciamo cosa succede con @repeat(3):
# Ciò che scrivi:
@repeat(3)
def greet(name):
print(f"Hello, {name}!")Passo 1: Python valuta repeat(3)
decorator = repeat(3) # La factory restituisce un decoratore (times=3 viene catturato)Passo 2: Python applica il decoratore a greet
def greet(name):
print(f"Hello, {name}!")
greet = decorator(greet) # Il decoratore restituisce un wrapper (func=greet viene catturato)Nota: a questo punto, greet ora si riferisce alla funzione wrapper. L’originale greet viene catturata in func.
Passo 3: Quando chiami greet("Alice"), esegue il wrapper
greet("Alice") # In realtà chiama wrapper("Alice")
# wrapper usa i valori catturati 'times' e 'func'Perché tre livelli?
Ogni livello cattura informazioni diverse tramite le chiusure:
def repeat(times): # Cattura: times
def decorator(func): # Cattura: func (e ricorda times)
def wrapper(*args, **kwargs): # Cattura: times, func, e riceve args
for i in range(times): # Usa il 'times' catturato
result = func(*args, **kwargs) # Usa 'func' e 'args' catturati
return result
return wrapper
return decorator- Livello 1 cattura la configurazione (
times) - Livello 2 cattura la funzione da decorare (
func) - Livello 3 riceve gli argomenti quando viene chiamata (
args,kwargs)
Senza tutti e tre i livelli, non potremmo avere un decoratore configurabile che ricorda sia le sue impostazioni sia la funzione che sta decorando.
Esempio 1: Un decoratore di logging configurabile
Ecco un esempio pratico di decoratore di logging che accetta configurazione:
def log_with_prefix(prefix="LOG"):
"""Factory di decoratori che crea un decoratore di logging con un prefisso personalizzato."""
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() # Usa il prefisso predefinito
def get_average(numbers):
return sum(numbers) / len(numbers)
# Test delle funzioni decorate
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.0Nota che:
@log_with_prefix(prefix="INFO")usa un prefisso personalizzato@log_with_prefix()usa il prefisso predefinito "LOG"- Devi includere le parentesi anche quando usi i valori predefiniti
Esempio 2: Un decoratore con più argomenti
Ecco un decoratore che valida intervalli numerici:
def validate_range(min_value=None, max_value=None):
"""
Factory di decoratori che valida che gli argomenti numerici siano entro un intervallo.
Args:
min_value: Valore minimo consentito (inclusivo)
max_value: Valore massimo consentito (inclusivo)
"""
def decorator(func):
def wrapper(*args, **kwargs):
# Controlla tutti gli argomenti numerici
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):
"""Calcola la percentuale."""
return (value / total) * 100
@validate_range(min_value=0)
def calculate_age(birth_year, current_year):
"""Calcola l'età a partire dall'anno di nascita."""
return current_year - birth_year
# Test con input validi
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 con input non validi
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 0Quando usare decoratori con argomenti
Usa decoratori con argomenti quando:
- Devi configurare il comportamento del decoratore
- Lo stesso decoratore deve funzionare in modo diverso in contesti diversi
- Vuoi rendere i decoratori più riutilizzabili e flessibili
Esempi comuni includono:
- Decoratori di retry con tentativi e ritardi configurabili
- Decoratori di logging con livelli o formati di log configurabili
- Decoratori di validazione con regole configurabili
- Decoratori di caching con dimensioni della cache o tempi di scadenza configurabili
- Rate limiting con limiti configurabili
Una nota sulla complessità
I decoratori con argomenti aggiungono un livello extra di complessità. Quando li scrivi:
- Usa nomi dei parametri chiari e descrittivi
- Fornisci valori predefiniti sensati
- Includi docstring che spiegano i parametri
- Valuta se la flessibilità aggiuntiva vale la complessità
Per casi semplici, un decoratore senza argomenti è spesso più chiaro e più facile da capire.
I decoratori sono uno strumento potente per scrivere codice Python pulito e manutenibile. Ti permettono di separare le responsabilità trasversali (come logging, timing e validazione) dalla logica di business principale, rendendo il codice più facile da leggere, testare e modificare. Man mano che continuerai a programmare in Python, troverai i decoratori usati ampiamente in framework e librerie, e scoprirai molte opportunità per scrivere i tuoi decoratori per risolvere problemi comuni in modo elegante.