ProgrammazioneSviluppatore Python

Che cosa sono i decorator in Python con argomenti, come si implementano, dove il loro utilizzo è giustificato e quali sono i dettagli importanti nella scrittura di propri decorator con parametri?

Supera i colloqui con l'assistente IA Hintsage

Risposta

Storia del problema

I decorator sono uno degli strumenti più potenti di Python, che permettono di modificare il comportamento di funzioni o metodi. A volte è necessario non solo "incapsulare" una funzione, ma configurare il decorator con parametri (argomenti). Questi casi si verificano nel logging, nei controlli di tempo, nelle restrizioni di accesso, ecc.

Problema

I normali decorator accettano solo una funzione, che deve essere incapsulata. Quando è necessario passare parametri al decorator stesso, la sintassi diventa più complessa, il che porta spesso a errori, specialmente con funzioni nidificate e passaggio di *args/**kwargs.

Soluzione

Un decorator con parametri è implementato da una funzione di ordine superiore: prima viene chiamata la funzione "decoratrice" esterna con argomenti, che crea e restituisce il decorator stesso, e il decorator poi riceve la funzione e restituisce l'incapsulamento:

def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"Hello, {name}!") greet("Python") # Output: Hello, Python! (3 volte)

Caratteristiche principali:

  • Il decorator con parametri è sempre implementato attraverso una tripla nidificazione di funzioni
  • Il wrapper deve restituire il risultato, passando correttamente *args/**kwargs
  • Non dimenticate di usare functools.wraps per mantenere i metadati

Domande insidiose.

Perché non si può implementare un decorator con parametri come un normale decorator?

Se si utilizza @decorator, Python passa la funzione come argomento al decorator. Se si aggiungono le parentesi (@decorator()), Python prima chiama la funzione, e solo il suo risultato viene interpretato come decorator.

def deco(func): # decorator normale: @deco def deco_with_args(arg): # decorator con argomento: @deco_with_args(arg)

In cosa differiscono sostanzialmente i decorator con argomenti dai decorator senza argomenti a livello di chiamata?

I decorator senza argomenti ricevono una funzione come input, mentre i decorator con argomenti non ricevono una funzione, ma parametri, e restituiscono un decorator.

Come applicare correttamente functools.wraps e perché farlo?

functools.wraps(func) nel wrapper mantiene il nome, la stringa di documentazione e altri metadati della funzione originale, altrimenti tutte queste informazioni verrebbero sovrascritte nel wrapper, ostacolando il debug e l'introspezione.

import functools def deco_with_args(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator

Errori comuni e anti-patterns

  • Si dimenticano della necessità di tre funzioni nidificate, ne fanno solo due (o addirittura una), il risultato non sarà un decorator
  • Non passano *args/**kwargs all'interno del wrapper
  • Perdono informazioni meta della funzione a causa della mancanza di functools.wraps

Esempio pratico

Caso negativo

Implementazione di un decorator senza tenere conto dei parametri o con un numero errato di funzioni nidificate:

def log(level): def wrapper(func): # errore — il wrapper deve essere più profondo print(f"Log: {level}") func() # La funzione non viene restituita come decorator return wrapper @log("INFO") def action(): print("Work!")

Vantaggi:

  • Sembra semplice

Svantaggi:

  • Il decorator non funziona, la funzione viene chiamata al momento della decorazione, non quando viene chiamato action()

Caso positivo

Utilizzo di functools.wraps e corrette funzioni nidificate:

import functools def timer(units): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) end = time.time() if units == 'ms': duration = (end - start) * 1000 else: duration = end - start print(f"Durata: {duration:.4f} {units}") return result return wrapper return decorator @timer('ms') def op(): sum(range(1000)) op()

Vantaggi:

  • Struttura corretta, facilmente espandibile, log puliti

Svantaggi:

  • Più difficile da leggere, soprattutto per i principianti