Python & AI Tutorials Logo
Programmazione Python

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:

python
# 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: 60

Questo approccio presenta diversi problemi:

  1. Duplicazione del codice: le righe di logging sono ripetute in ogni funzione.
  2. Mescolare responsabilità: il codice di logging è mescolato con la logica di business.
  3. Difficile da mantenere: se vuoi cambiare il formato del logging, devi aggiornare ogni funzione.
  4. 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:

python
# 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: 60

La 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:

python
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:

python
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:

python
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: 50

La 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:

python
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: greeting

Tracciamo cosa succede:

  1. simple_wrapper riceve say_hello come original_func
  2. Crea una nuova funzione wrapper che:
    • Stampa "Before calling the function"
    • Chiama original_func() (che è say_hello)
    • Stampa "After calling the function"
    • Restituisce il risultato
  3. simple_wrapper restituisce la funzione wrapper
  4. Quando chiamiamo wrapped_hello(), in realtà stiamo chiamando wrapper, che chiama l’originale say_hello al 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:

python
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:

  • *args raccoglie tutti gli argomenti posizionali in una tupla
  • **kwargs raccoglie 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:

python
greet = flexible_wrapper(greet)

Useremo la sintassi @:

python
@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:

python
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.0

La sintassi @log_call è esattamente equivalente a scrivere:

python
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:

python
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: 30

Quando i decoratori sono impilati, vengono applicati dal basso verso l’alto (prima quello più vicino alla funzione):

python
@timer          # Applicato per secondo (strato più esterno)
@log_call       # Applicato per primo (più vicino alla funzione)
def process_data(items):
    pass

Questo equivale a:

python
process_data = timer(log_call(process_data))

Ordine di applicazione (dal basso verso l’alto):

  1. @log_call avvolge per primo la funzione originale
  2. @timer avvolge il risultato (avvolge la funzione già avvolta)

Ordine di esecuzione (dall’alto verso il basso, dallo strato più esterno a quello più interno):

  1. parte il wrapper di timer (più esterno, esegue per primo)
  2. parte il wrapper di log_call (wrapper interno)
  3. esegue la funzione originale
  4. termina il wrapper di log_call
  5. 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:

Funzione originale
process_data

Passo 1: @log_call(decoratore in basso)

log_call avvolge l'originale

Passo 2: @timer(decoratore in alto)

timer avvolge il wrapper di log_call

Finale: timer avvolge log_call avvolge l'originale

Flusso di esecuzione:

Chiama process_data

1. parte il wrapper di timer
2. parte il wrapper di log_call
3. esegue la funzione originale
4. termina il wrapper di log_call
5. termina il wrapper di timer

Restituisce il risultato

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:

python
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 ID

Questo decoratore:

  • Aggiunge timestamp a tutti i messaggi di log
  • Registra sia i completamenti riusciti sia le eccezioni
  • Rilancia le eccezioni dopo averle registrate (usando raise senza argomento)
  • Usa un blocco try/except per 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:

python
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 digits

Questo 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:

python
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 -10

Questo 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:

python
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:

  1. Valuta @log_call → Risultato: log_call stesso (l’oggetto funzione)
  2. Applica a greet: greet = log_call(greet)

B) Factory di decoratori:

Basato su questo esempio:

python
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:

  1. Valuta @repeat(3) → Risultato: repeat(3) viene chiamata, e restituisce una funzione decoratore
  2. 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:

python
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 decorator

Livello 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):

python
# Ciò che scrivi:
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

Passo 1: Python valuta repeat(3)

python
decorator = repeat(3)  # La factory restituisce un decoratore (times=3 viene catturato)

Passo 2: Python applica il decoratore a greet

python
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

python
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:

python
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:

python
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.0

Nota 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:

python
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 0

Quando 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.


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