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

URL: https://skillaria.top/it/guides/python/decoratori-python

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:

| Decoratore | Che cosa fa |
| --- | --- |

| `@property` | trasforma un metodo in un attributo di sola lettura; con `@x.setter` diventa un attributo controllato |

| `@staticmethod` | metodo di classe senza `self`, una normale funzione nello spazio dei nomi della classe |

| `@classmethod` | il metodo riceve la classe `cls` invece dell'istanza — così si scrivono i costruttori alternativi |

| `@functools.cache`, `@functools.lru_cache` | memorizzano i risultati in base agli argomenti |

| `@functools.wraps` | copia i metadati della funzione sull'involucro |

| `@dataclasses.dataclass` | genera `__init__`, `__repr__`, `__eq__` per la classe |

| `@contextlib.contextmanager` | trasforma 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': }
```

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.

## FAQ

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


---
Source: Skillaria.Top (https://skillaria.top)
