Basi

Decoratori in Python: che cosa sono, come funzionano e come scriverne uno

Aggiornato: · Skillaria.Top

Un decoratore in Python è una funzione che riceve un'altra funzione e restituisce una nuova funzione, di solito un involucro che aggiunge un comportamento prima o dopo la chiamata: logging, misura del tempo, cache, controllo dei permessi. La scrittura @decoratore sopra def è una scorciatoia per func = decoratore(func); per conservare il nome e la docstring della funzione originale si usa functools.wraps.

Un decoratore in Python è una funzione che riceve un'altra funzione e ne restituisce una nuova, di solito un involucro intorno a quella originale. In questo modo si aggiunge un comportamento — logging, misura del tempo, cache, ripetizione dei tentativi — senza cambiare il codice della funzione. I decoratori si scrivono con la riga @nome sopra la dichiarazione della funzione e si incontrano ovunque: in Flask e FastAPI, in pytest, nella libreria standard.

Che cos'è un decoratore in parole semplici

Pensate a un regalo e alla carta da regalo. Il regalo (la funzione) resta lo stesso, ma intorno compare uno strato che entra in azione per primo. Il decoratore è proprio questo involucro: quando la funzione viene chiamata, per prima cosa esegue il codice dell'involucro, che richiama la funzione originale e può fare qualcosa con il suo risultato.

Funziona perché in Python le funzioni sono oggetti normali. Si possono passare ad altre funzioni, restituire da funzioni e dichiarare dentro altre funzioni. Su queste tre proprietà si basano i decoratori.

Come funziona un decoratore: la @ è una scorciatoia

Prendiamo un decoratore semplicissimo, che stampa un messaggio prima e dopo la chiamata:

def log_calls(func):
    def wrapper():
        print(f"Chiamo {func.__name__}")
        result = func()
        print(f"{func.__name__} completata")
        return result
    return wrapper

@log_calls
def hello():
    print("Ciao!")

hello()
Chiamo hello
Ciao!
hello completata

La scrittura @log_calls sopra def è del tutto equivalente a questa riga, scritta dopo la dichiarazione:

def hello():
    print("Ciao!")

hello = log_calls(hello)

Ora il nome hello punta a wrapper, mentre la funzione originale vive dentro la closure: wrapper ricorda la variabile func della funzione esterna. Un dettaglio importante: il decoratore viene applicato una sola volta, nel momento in cui viene eseguito il def, cioè all'import del modulo; l'involucro invece entra in azione a ogni chiamata.

Closure e decoratori

Una closure è una funzione annidata che ricorda le variabili della funzione esterna anche dopo che questa è terminata. Senza le closure i decoratori non esisterebbero: è così che wrapper accede a func. Dentro una closure si può conservare anche uno stato, per esempio un contatore di chiamate:

def count_calls(func):
    calls = 0
    def wrapper(*args, **kwargs):
        nonlocal calls
        calls += 1
        print(f"{func.__name__}: chiamata n. {calls}")
        return func(*args, **kwargs)
    return wrapper

Come scrivere un decoratore in Python: schema universale

Il primo esempio ha due difetti: l'involucro non accetta argomenti e «cancella» il nome della funzione originale. Lo schema funzionante, da cui conviene partire per qualsiasi decoratore, è questo:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # codice prima della chiamata
        result = func(*args, **kwargs)
        # codice dopo la chiamata
        return result
    return wrapper

Tre regole dello schema:

  1. *args, **kwargs — l'involucro accetta qualsiasi argomento e lo inoltra senza modifiche.
  2. return result — non dimenticate di restituire il risultato, altrimenti la funzione decorata restituirà sempre None.
  3. @functools.wraps(func) — trasferisce i metadati della funzione originale all'involucro.

Esempio di decoratore: misura del tempo di esecuzione:

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} ha impiegato {elapsed:.3f} s")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)   # slow_sum ha impiegato 0.142 s

A che cosa serve functools.wraps

Senza wraps la funzione decorata perde il nome, la docstring e gli altri attributi: all'esterno si vede wrapper:

def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain
def add(a, b):
    """Somma due numeri."""
    return a + b

print(add.__name__, add.__doc__)   # wrapper None

Con @functools.wraps(func) l'involucro riceve __name__, __doc__, __module__, __qualname__, __annotations__ della funzione originale, e nell'attributo __wrapped__ resta il riferimento all'originale. È importante per il debug e per i traceback, per help(), per i framework che registrano le funzioni per nome e per inspect.signature, che mostra la firma reale.

Decoratore con parametri (con argomenti)

A volte il decoratore ha bisogno di impostazioni: @retry(times=3), @app.route("/"). Per questo si aggiunge un livello di annidamento in più. La funzione esterna accetta i parametri e restituisce il vero decoratore: questa costruzione si chiama fabbrica di decoratori.

import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def ping():
    print("ping")

ping()   # ping, ping, ping — tre righe

Ordine delle chiamate: repeat(times=3) restituisce decorator, che viene applicato a ping e restituisce wrapper. In altre parole, @repeat(times=3) equivale a ping = repeat(times=3)(ping).

Un esempio pratico: ripetere la chiamata in caso di errore:

import functools
import time

def retry(times=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if attempt == times:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
    ...

Più decoratori sulla stessa funzione

I decoratori si possono impilare uno sopra l'altro. Vengono applicati dal basso verso l'alto: quello più vicino a def è il primo.

@timer
@log_calls
def report():
    ...

# equivale a:
report = timer(log_calls(report))

Durante la chiamata lo strato esterno (timer) entra in azione per primo, quindi misurerà anche il tempo di lavoro di log_calls. L'ordine conta: per esempio, se un decoratore verifica i permessi e un altro mette in cache il risultato, la sequenza cambia il comportamento.

Decoratori standard e integrati di Python

Molti decoratori utili sono già nel linguaggio o nella libreria standard:

DecoratoreChe cosa fa
@propertytrasforma un metodo in un attributo di sola lettura; con @x.setter diventa un attributo controllato
@staticmethodmetodo di classe senza self, una normale funzione nello spazio dei nomi della classe
@classmethodil metodo riceve la classe cls invece dell'istanza — così si scrivono i costruttori alternativi
@functools.cache, @functools.lru_cachememorizzano i risultati in base agli argomenti
@functools.wrapscopia i metadati della funzione sull'involucro
@dataclasses.dataclassgenera __init__, __repr__, __eq__ per la classe
@contextlib.contextmanagertrasforma un generatore in un gestore di contesto per with

Cache: functools.cache e lru_cache

Un decoratore di cache ricorda il risultato per ogni insieme di argomenti e, alla chiamata successiva con gli stessi argomenti, lo restituisce senza ricalcolarlo. L'esempio classico sono i numeri di Fibonacci ricorsivi, che senza cache richiedono un tempo esponenziale:

import functools

@functools.cache
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(100))   # 354224848179261915075

functools.cache (comparso in Python 3.9) conserva tutti i risultati senza limiti. @functools.lru_cache(maxsize=128) tiene solo le ultime voci ed elimina le più vecchie: è più sicuro per i servizi che girano a lungo. Le statistiche si leggono con fib.cache_info(), la cache si svuota con fib.cache_clear(). Gli argomenti di queste funzioni devono essere hashable: una lista come argomento genera TypeError: unhashable type: 'list'. Inoltre la cache ha senso solo per le funzioni pure, cioè quelle che con gli stessi argomenti restituiscono sempre lo stesso risultato e non hanno effetti collaterali.

Che cosa fa il decoratore property

@property permette di usare un metodo come un attributo, senza parentesi, e di controllare l'assegnazione se serve:

class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("Il saldo non può essere negativo")
        self._balance = value

acc = Account(100)
print(acc.balance)   # 100
acc.balance = -5     # ValueError

staticmethod e classmethod

class Money:
    def __init__(self, cents):
        self.cents = cents

    @classmethod
    def from_euros(cls, euros):
        return cls(round(euros * 100))

    @staticmethod
    def is_valid(value):
        return value >= 0

m = Money.from_euros(12.5)
print(m.cents, Money.is_valid(-1))   # 1250 False

Proprio @classmethod permette al metodo di lavorare con la classe stessa e non con le sue istanze: from_euros crea correttamente anche un oggetto di una sottoclasse.

Decoratori di classe e classi usate come decoratori

Un decoratore si può applicare a una classe: riceve la classe e restituisce una classe (la stessa o modificata). È così che funziona @dataclass. Un esempio semplice è un registro di classi:

registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class CsvExporter:
    pass

print(registry)   # {'CsvExporter': <class '__main__.CsvExporter'>}

Il caso inverso è una classe usata come decoratore. Va bene qualsiasi oggetto con il metodo __call__; lo stato è comodo da conservare negli attributi dell'istanza:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)

@CountCalls
def tick():
    pass

tick(); tick()
print(tick.calls)   # 2

Decoratore asincrono

Per le funzioni async def anche l'involucro deve essere una corrutina e attendere il risultato con await; altrimenti restituirebbe un oggetto corrutina non eseguito:

import functools

def log_async(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        print(f"Avvio {func.__name__}")
        result = await func(*args, **kwargs)
        print(f"Fatto {func.__name__}")
        return result
    return wrapper

A che cosa servono i decoratori in Python

  • Logica trasversale — logging, metriche, misura dei tempi senza copiare il codice in ogni funzione.
  • Cache — @functools.cache per i calcoli costosi e puri.
  • Controlli — permessi, validazione degli argomenti, autorizzazione nei gestori web.
  • Registrazione — rotte in Flask e FastAPI (@app.get("/")), fixture di pytest.
  • Affidabilità — ripetizioni dei tentativi, timeout, gestione degli errori.

Il vantaggio principale è la separazione delle responsabilità: la funzione si occupa del proprio compito, mentre il comportamento accessorio si aggiunge con una sola riga e si rimuove con la stessa facilità.

Errori tipici

  • L'involucro non restituisce il risultato — dopo la decorazione la funzione restituisce None.
  • Manca *args, **kwargs — TypeError quando si chiama la funzione con argomenti.
  • Dimenticato wraps — nomi e docstring sbagliati, confusione nei log.
  • Parentesi della fabbrica — @repeat al posto di @repeat(3): la funzione finisce nel parametro times.
  • Codice pesante nel corpo del decoratore — viene eseguito all'import del modulo, non alla chiamata.

Conclusioni

I decoratori in Python sono funzioni di ordine superiore: ricevono una funzione, restituiscono un involucro, e la @ è solo una forma breve di assegnazione. Partite dallo schema con *args, **kwargs, return result e functools.wraps; per le impostazioni aggiungete il livello della fabbrica. Funzioni, closure e annotazioni, su cui si reggono i decoratori, sono trattati in pratica nel corso di Python.

Domande frequenti

Si può rimuovere un decoratore da una funzione?

Se il decoratore usa functools.wraps, l'originale è accessibile tramite l'attributo __wrapped__: func.__wrapped__(...) chiama la funzione senza involucro. Altrimenti bisogna eliminare la riga con la @ dal codice.

Quando viene eseguito il codice del decoratore?

Il corpo del decoratore viene eseguito una sola volta, al momento della dichiarazione della funzione, di solito durante l'import del modulo. Il codice dentro wrapper viene eseguito a ogni chiamata della funzione decorata.

Si possono decorare i metodi di una classe?

Sì, un decoratore normale con *args, **kwargs funziona anche con i metodi: self semplicemente diventa il primo elemento di args.

In che cosa un decoratore si differenzia dall'ereditarietà?

L'ereditarietà modifica il comportamento di una classe attraverso una gerarchia, mentre il decoratore avvolge dall'esterno una funzione o una classe specifica e si combina facilmente con altri decoratori senza toccare il codice.

Impara l’argomento in video

Questo argomento è trattato in modo approfondito, con pratica, nel corso «Python 3.15: от новичка до профи».

Vai al corso