Menu

Decoratori in Python: @funzioni, argomenti e functools.wraps

Cosa sono davvero i decoratori in Python, come scriverne di tuoi e gli schemi (argomenti, impilamento, wraps) che li rendono utili.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un decoratore è una funzione che avvolge una funzione

Detta così sembra astratta, ma il meccanismo è semplice. Un decoratore riceve una funzione e restituisce una funzione. La funzione restituita di solito chiama quella originale, con un comportamento in più attorno alla chiamata.

L'esempio più breve possibile:

shout è il decoratore. Riceve una funzione (greet), costruisce una nuova funzione (wrapper) che chiama l'originale e mette il risultato in maiuscolo, e la restituisce. Riassegnare greet = shout(greet) sostituisce l'originale con la versione avvolta.

Questo schema di riassegnazione è così comune che Python gli ha dato una sintassi dedicata.

La @ è zucchero sintattico per una riassegnazione

@name sulla riga sopra un def equivale a name = name(...) subito dopo la definizione della funzione:

@shout si legge "applica il decoratore shout a questa funzione". Python esegue greet = shout(greet) subito dopo il def: stesso meccanismo di prima, meno da scrivere.

Quando vedi @name, sostituiscilo mentalmente con function = name(function). La sintassi significa solo questo.

Gestire gli argomenti

La maggior parte delle funzioni riceve argomenti. Un decoratore utilizzabile li inoltra. L'idioma è *args, **kwargs, il modo in cui Python accetta argomenti di qualsiasi tipo, perché al wrapper non deve importare cosa si aspetta la funzione avvolta:

*args cattura tutti gli argomenti posizionali. **kwargs cattura tutti gli argomenti keyword. Il wrapper inoltra tutto alla funzione avvolta senza modifiche, poi fa il lavoro extra per cui esiste il decoratore: qui, mettere il risultato in maiuscolo.

Questa è la forma che prende la maggior parte dei decoratori reali.

Un esempio più utile: misurare il tempo

Stampa quanto ci mette una funzione:

Lo schema (esegui qualcosa prima della chiamata, esegui qualcosa dopo) è quello che finisce per fare la maggior parte dei decoratori. Logging, controlli di autorizzazione, nuovi tentativi e validazione degli input seguono tutti la stessa forma.

Conservare l'identità originale: functools.wraps

Decorare una funzione la sostituisce, quindi la funzione avvolta perde i suoi attributi originali __name__ e __doc__:

Ora greet.__name__ è "wrapper" e la docstring è sparita. Questo rompe help(), i traceback e qualsiasi strumento che ispezioni la funzione.

La soluzione è una riga: @functools.wraps(func) sulla funzione interna copia i metadati.

Aggiungi sempre @wraps(func) alla funzione interna. Non costa niente e ti evita sessioni di debug piene di sorprese più avanti.

Decoratori con argomenti

A volte è il decoratore stesso ad aver bisogno di una configurazione: "riprova questa funzione fino a 3 volte", "fai log a livello DEBUG". Questo significa un ulteriore livello di annidamento: una funzione esterna che riceve gli argomenti e restituisce un decoratore.

Tre livelli sembrano tanti. Leggili dall'esterno verso l'interno:

  1. repeat(times=3) è una chiamata di funzione. Restituisce decorator.
  2. decorator è il vero decoratore: riceve una funzione e ne restituisce una avvolta.
  3. wrapper è la funzione avvolta che viene eseguita al momento della chiamata.

Questa forma sta dietro a @retry(times=5), @cache(maxsize=100) e ai decoratori dei framework come @app.route("/users"). Una volta riconosciuto lo schema a tre livelli, tutta la famiglia si legge allo stesso modo.

Impilare i decoratori

Puoi applicare più di un decoratore a una funzione. Si impilano dal basso verso l'alto: quello più vicino al def viene applicato per primo:

add_exclaim avvolge per primo e aggiunge il !. Poi shout avvolge il risultato e mette tutto in maiuscolo. L'output è HI, ROSA!.

L'ordine conta. Invertendo la pila ottieni HI, ROSA! con il punto esclamativo aggiunto dopo il maiuscolo: qui il risultato è visivamente identico, ma immagina un decoratore che formatta JSON: eseguirlo prima o dopo un decoratore che registra l'input può dare risultati molto diversi.

Decoratori integrati che incontrerai

Python e la sua libreria standard includono una manciata di decoratori che vedrai nel codice reale:

  • @property trasforma un metodo in un attributo calcolato.
  • @staticmethod indica un metodo che non usa né self né cls.
  • @classmethod riceve la classe come cls invece di un'istanza: ottimo per costruttori alternativi.
  • @functools.lru_cache memorizza i risultati, così le chiamate ripetute con gli stessi argomenti usano una cache.

I decoratori dei framework (@app.route, @pytest.fixture, @dataclass) seguono lo stesso meccanismo. Niente di speciale: solo funzioni che avvolgono funzioni.

Quando scriverne uno e quando no

Scrivi un decoratore quando vuoi applicare lo stesso comportamento a molte funzioni: misura del tempo, logging, nuovi tentativi, controlli di autorizzazione. Lo scopo è proprio tenere quel comportamento fuori dal corpo della funzione.

Lascia perdere il decoratore quando:

  • Il comportamento appartiene a una sola funzione specifica. Mettilo nella funzione.
  • Ti serve solo per i test. Una fixture o un parametro sono più chiari.
  • Hai la tentazione di impilarne quattro o cinque. A quel punto il flusso di controllo è nascosto nella catena di decoratori: chi legge deve srotolare ogni livello per capire cosa viene eseguito davvero. Una semplice funzione di supporto può leggersi meglio.

I decoratori sono uno strumento affilato. Usati bene, mantengono il codice DRY e l'intenzione evidente. Usati male, nascondono cosa sta facendo il programma. Quando decidi se usarne uno, pendi verso l'"evidente".

Prossimo argomento: i type hint

I decoratori sono un posto in cui capita spesso di incontrare i type hint nel codice reale: le funzioni wrapper spesso annotano la propria firma. I type hint sono una piccola funzionalità che ripaga in fretta, e arrivano subito dopo.

Domande frequenti

Cos'è un decoratore in Python?

Un decoratore è una funzione che riceve un'altra funzione e ne restituisce una nuova, di solito una che avvolge l'originale con un comportamento in più. Lo applichi con @decorator_name sulla riga sopra un def. La sintassi @ è una scorciatoia per func = decorator_name(func).

A cosa servono i decoratori in Python?

Ad aggiungere comportamento attorno a una funzione senza modificarne il corpo: logging, misura del tempo, cache, controlli di autenticazione, validazione degli input, nuovi tentativi. I framework li usano moltissimo: @app.route(...) in Flask, @pytest.fixture in pytest, e @property e @staticmethod sono integrati nel linguaggio.

Posso scrivere un decoratore tutto mio?

Sì. Un decoratore è solo una funzione che riceve una funzione e restituisce una funzione. La maggior parte dei decoratori personalizzati avvolge la chiamata originale in una piccola funzione interna che fa qualcosa prima, dopo o attorno a essa. Usa functools.wraps sulla funzione interna per conservare il nome e la docstring della funzione originale.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA