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:
repeat(times=3)è una chiamata di funzione. Restituiscedecorator.decoratorè il vero decoratore: riceve una funzione e ne restituisce una avvolta.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:
@propertytrasforma un metodo in un attributo calcolato.@staticmethodindica un metodo che non usa néselfnécls.@classmethodriceve la classe comeclsinvece di un'istanza: ottimo per costruttori alternativi.@functools.lru_cachememorizza 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.