Se hai usato Flask o FastAPI per più di una settimana, hai già usato i decorator — @app.route, @login_required, @pytest.mark.parametrize. Sembrano magia la prima volta, e poi qualcuno spiega cosa sta succedendo davvero e scatta immediatamente. Un decorator è solo una funzione che avvolge un'altra funzione. La sintassi @ è puro zucchero sintattico — @my_decorator sopra una definizione di funzione è esattamente equivalente a scrivere func = my_decorator(func) dopo di essa. Questo è il segreto totale. Il resto sono solo pattern costruiti su quell'unica idea. Questo articolo costruisce il modello mentale da zero, poi illustra i pattern che userai davvero: @functools.wraps, decorator con argomenti, decorator basati su classi, e una manciata di esempi del mondo reale inclusi @lru_cache, @dataclass, e un @retry appropriato con backoff esponenziale.

Le Funzioni Sono Oggetti di Prima Classe

Prima che i decorator abbiano senso, devi avere ben chiaro un fatto su Python: le funzioni sono oggetti. Puoi assegnarle a variabili, passarle come argomenti ad altre funzioni, restituirle da funzioni, e memorizzarle in liste o dizionari. Non succede nulla di speciale solo perché qualcosa è una funzione.

python
def greet(name: str) -> str:
    return f"Hello, {name}"

# Assign to a variable — no () means we're not calling it, just referencing it
say_hello = greet
print(say_hello("Alice"))   # 'Hello, Alice'

# Pass a function as an argument
def run_twice(fn, value):
    return fn(value), fn(value)

run_twice(greet, "Bob")     # ('Hello, Bob', 'Hello, Bob')

# Return a function from another function — this is a "factory"
def make_prefixer(prefix: str):
    def prefixed_greet(name: str) -> str:
        return f"{prefix}, {name}"
    return prefixed_greet

morning_hello = make_prefixer("Good morning")
morning_hello("Carol")      # 'Good morning, Carol'

La funzione interna prefixed_greet "chiude" sulla variabile prefix dall'ambito che la contiene — anche dopo che make_prefixer è ritornata, la funzione interna ha ancora accesso a prefix. Questa è una chiusura, ed è il meccanismo che fa funzionare i decorator. La documentazione Python sulle regole di scoping spiega questo in dettaglio se vuoi il quadro completo.

Costruire un Decorator da Zero

Un decorator è una funzione che prende una funzione e restituisce una funzione (di solito modificata). Il classico primo esempio è un decorator di temporizzazione — avvolge qualsiasi funzione e registra quanto ci ha messo a girare.

python
import time
import functools

def timer(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = fn(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{fn.__name__} finished in {elapsed:.4f}s")
        return result
    return wrapper

@timer
def fetch_user_records(db, user_id: int):
    """Fetch all records for a given user from the database."""
    return db.query("SELECT * FROM records WHERE user_id = ?", user_id)

# Calling fetch_user_records is now: timer(fetch_user_records)(db, 42)
# Which is exactly what @timer desugars to

Alcune cose da notare: il wrapper usa *args, **kwargs per poter inoltrare qualsiasi combinazione di argomenti alla funzione originale senza conoscerne la firma. Cattura il valore di ritorno in result e lo restituisce, così la funzione avvolta si comporta ancora in modo identico dalla prospettiva del chiamante — ha solo un effetto collaterale di stampa in più. Rimuovi il timing, e hai lo scheletro di quasi ogni decorator che scriverai mai.

La regola di desugarizzazione: Ogni volta che vedi @some_decorator sopra una definizione di funzione, sostituiscila mentalmente con fn = some_decorator(fn) scritto immediatamente dopo il blocco def. I due sono esattamente equivalenti. Non c'è magia — è una chiamata di funzione.

Perché Devi Usare @functools.wraps

Nell'esempio sopra, c'è una riga @functools.wraps(fn) sul wrapper. Questa non è opzionale. Senza di essa, la tua funzione decorata perde la sua identità — i suoi attributi __name__, __doc__ e __qualname__ vengono tutti sostituiti con quelli della funzione interna wrapper. Questo causa rotture sottili in alcune situazioni reali:

  • Le docstring scompaiono. help(fetch_user_records) mostra la docstring vuota del wrapper invece di "Fetch all records for a given user...".
  • I traceback mentono. Quando viene sollevata un'eccezione all'interno della funzione avvolta, il traceback mostra wrapper invece del nome reale della funzione — difficile da debuggare.
  • L'introspezione si rompe. Strumenti come pytest, il sistema di routing di Flask, e inspect.signature() si basano tutti su __name__ e __wrapped__. Il router di Flask lancerà un'eccezione se due route condividono lo stesso nome (wrapper).
  • functools.lru_cache e strumenti simili usano l'identità della funzione per la chiave della cache — senza wraps, puoi ottenere sorprendenti collisioni nella cache.
python
import functools

# Without @functools.wraps — broken
def bad_timer(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@bad_timer
def process_batch(batch_id: int):
    """Process a single batch job."""
    pass

print(process_batch.__name__)   # 'wrapper'   ← wrong
print(process_batch.__doc__)    # None         ← docstring gone

# With @functools.wraps — correct
def good_timer(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@good_timer
def process_batch(batch_id: int):
    """Process a single batch job."""
    pass

print(process_batch.__name__)   # 'process_batch'  ← correct
print(process_batch.__doc__)    # 'Process a single batch job.'  ← preserved

functools.wraps è esso stesso un decorator — copia __module__, __name__, __qualname__, __annotations__, __doc__ e imposta __wrapped__ alla funzione originale. Usalo su ogni funzione wrapper, punto e basta. Non c'è motivo per non farlo.

Decorator con Argomenti

Un decorator semplice prende una funzione e restituisce una funzione. Un decorator con argomenti ha bisogno di un livello in più: una funzione che prende gli argomenti e restituisce un decorator. Sono tre livelli di nesting, e confonde quasi tutti la prima volta che lo vedono. Ecco un decorator @retry che ritenta una funzione fino a max_attempts volte in caso di eccezione:

python
import functools
import time

def retry(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0):
    """
    Retry a function on exception with exponential backoff.

    Usage:
        @retry(max_attempts=5, delay=0.5, backoff=2.0)
        def call_external_api(endpoint: str) -> dict:
            ...
    """
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            current_delay = delay
            last_exception = None

            for attempt in range(1, max_attempts + 1):
                try:
                    return fn(*args, **kwargs)
                except Exception as exc:
                    last_exception = exc
                    if attempt < max_attempts:
                        print(
                            f"{fn.__name__}: attempt {attempt}/{max_attempts} failed "
                            f"({exc!r}), retrying in {current_delay:.1f}s..."
                        )
                        time.sleep(current_delay)
                        current_delay *= backoff
                    else:
                        print(f"{fn.__name__}: all {max_attempts} attempts failed.")

            raise last_exception

        return wrapper
    return decorator

@retry(max_attempts=4, delay=0.5, backoff=2.0)
def fetch_price_data(ticker: str) -> dict:
    """Fetch stock price data from external API."""
    response = requests.get(f"https://api.example.com/prices/{ticker}", timeout=5)
    response.raise_for_status()
    return response.json()

La catena di chiamate quando Python elabora @retry(max_attempts=4, delay=0.5, backoff=2.0): prima viene chiamata retry(max_attempts=4, delay=0.5, backoff=2.0) e restituisce decorator. Poi viene chiamata decorator(fetch_price_data) e restituisce wrapper. Infine fetch_price_data viene ricollegata a wrapper. Quindi @retry(...) è fetch_price_data = retry(...)(fetch_price_data) — tre chiamate, due livelli di wrapping. Una volta che vedi quel pattern, le decorator factory smettono di essere fonte di confusione.

Decorator Basati su Classi

Puoi anche implementare un decorator come classe definendo __call__. Questo è utile quando il decorator deve mantenere stato tra le chiamate — un contatore di chiamate, una cache, pool di connessioni — perché le variabili di istanza sono una casa più naturale per quello stato rispetto alle variabili di chiusura.

python
import functools
import time

class RateLimiter:
    """
    Decorator that limits how often a function can be called.
    Raises RuntimeError if the function is called within `min_interval` seconds
    of the previous call.
    """

    def __init__(self, min_interval: float):
        self.min_interval = min_interval
        self._last_called: float = 0.0

    def __call__(self, fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            now = time.monotonic()
            since_last = now - self._last_called
            if since_last < self.min_interval:
                wait = self.min_interval - since_last
                raise RuntimeError(
                    f"{fn.__name__} called too soon — "
                    f"wait {wait:.2f}s before calling again."
                )
            self._last_called = now
            return fn(*args, **kwargs)
        return wrapper

# Usage — one call per 2 seconds
@RateLimiter(min_interval=2.0)
def send_sms_alert(phone: str, message: str) -> None:
    """Send an SMS alert via the gateway API."""
    sms_gateway.send(phone, message)

@RateLimiter(min_interval=2.0) funziona esattamente come una decorator factory: RateLimiter(2.0) costruisce un'istanza, poi quella istanza viene chiamata con send_sms_alert perché ha __call__. L'istanza memorizza _last_called come attributo — nessuna gestione di variabili di chiusura necessaria. Vedi PEP 318 (la proposta originale del decorator) per la motivazione di design dietro la sintassi @, e PEP 614 per la grammatica del decorator rilassata atterrata in Python 3.9.

Decorator del Mondo Reale dalla Libreria Standard

Prima di scrivere i tuoi, controlla se la stdlib ha già ciò di cui hai bisogno. Tre decorator in functools compaiono costantemente nel codice Python di produzione.

python
from functools import lru_cache, cache
import requests

# @cache — unbounded memoisation (Python 3.9+)
# Caches every unique set of arguments forever.
# Good for pure functions with a small domain of inputs.
@cache
def get_country_name(country_code: str) -> str:
    """Look up a country name from ISO 3166 code. Cached after first call."""
    response = requests.get(f"https://restcountries.com/v3.1/alpha/{country_code}")
    return response.json()[0]["name"]["common"]

get_country_name("DE")   # hits the API
get_country_name("DE")   # served from cache, no network call

# @lru_cache(maxsize=N) — bounded LRU cache
# Evicts least-recently-used entries once the cache hits `maxsize`.
# Better when the input domain is large and memory is a concern.
@lru_cache(maxsize=256)
def compute_discount(base_price: float, tier: str) -> float:
    """Heavy computation — price varies by tier. Cache the top 256 combinations."""
    discount_table = load_discount_table()          # expensive DB call
    rate = discount_table.get(tier, 0.0)
    return round(base_price * (1 - rate), 2)

# Inspect cache performance
print(compute_discount.cache_info())
# CacheInfo(hits=142, misses=14, maxsize=256, currsize=14)

@dataclass è in una categoria diversa — è un decorator di classe che genera automaticamente __init__, __repr__ e __eq__ dalle tue annotazioni di campo. Elimina una quantità significativa di boilerplate per le classi che contengono dati:

python
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime

@dataclass
class WebhookEvent:
    event_type: str
    source_id: int
    payload: dict
    received_at: datetime = field(default_factory=datetime.utcnow)
    retry_count: int = 0
    error_message: Optional[str] = None

# @dataclass generates all of this for free:
# - __init__(self, event_type, source_id, payload, received_at=..., retry_count=0, error_message=None)
# - __repr__ that shows all fields
# - __eq__ that compares field-by-field

event = WebhookEvent(event_type="order.created", source_id=9912, payload={"order_id": 44501})
print(event)
# WebhookEvent(event_type='order.created', source_id=9912, payload={...}, retry_count=0, ...)

Il decorator @property trasforma un metodo in un accessor in stile attributo — i chiamanti leggono user.display_name invece di user.get_display_name(). Combinalo con @property.setter per validare in scrittura:

python
class UserProfile:
    def __init__(self, first_name: str, last_name: str, email: str):
        self._first_name = first_name
        self._last_name = last_name
        self._email = email.strip().lower()

    @property
    def display_name(self) -> str:
        return f"{self._first_name} {self._last_name}"

    @property
    def email(self) -> str:
        return self._email

    @email.setter
    def email(self, value: str) -> None:
        if "@" not in value:
            raise ValueError(f"Invalid email address: {value!r}")
        self._email = value.strip().lower()

profile = UserProfile("Alice", "Smith", "  [email protected]  ")
print(profile.display_name)   # 'Alice Smith'
print(profile.email)          # '[email protected]'

profile.email = "[email protected]"    # setter runs validation
profile.email = "not-an-email"           # raises ValueError

Un Decorator @require_auth per i Gestori di Route

I controlli di autenticazione nei framework web sono un caso d'uso da manuale per i decorator. Invece di duplicare il controllo "l'utente è loggato?" in cima a ogni gestore di route, lo scrivi una volta come decorator e lo applichi dove necessario. Ecco il pattern, scritto per funzionare con Flask ma trasferibile a qualsiasi framework:

python
import functools
from flask import request, jsonify, g

def require_auth(fn):
    """
    Decorator that validates a Bearer token before the route handler runs.
    Sets g.current_user on success; returns 401 JSON on failure.
    """
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        auth_header = request.headers.get("Authorization", "")
        if not auth_header.startswith("Bearer "):
            return jsonify({"error": "Missing or malformed Authorization header"}), 401

        token = auth_header[len("Bearer "):]
        user = verify_token(token)          # your token validation logic
        if user is None:
            return jsonify({"error": "Invalid or expired token"}), 401

        g.current_user = user               # available to the route handler
        return fn(*args, **kwargs)
    return wrapper

# Usage — the auth check runs before the handler body
@app.route("/api/v1/reports/<int:report_id>")
@require_auth
def get_report(report_id: int):
    report = Report.query.get_or_404(report_id)
    if report.owner_id != g.current_user.id:
        return jsonify({"error": "Forbidden"}), 403
    return jsonify(report.to_dict())

Nota l'ordine: @app.route va prima (più esterno), @require_auth va secondo. Questo è importante — vedi la prossima sezione. Il pattern si estende naturalmente: potresti aggiungere un decorator factory @require_role("admin") che controlla g.current_user.role dopo che @require_auth ha già verificato che l'utente esiste.

Impilare Decorator — L'Ordine Conta

Quando impili più decorator, si applicano dal basso verso l'alto (il decorator più vicino alla funzione si applica per primo), ma si eseguono dall'alto verso il basso quando la funzione viene chiamata. Questo coglie le persone di sorpresa.

python
@decorator_a
@decorator_b
@decorator_c
def my_function():
    pass

# This is exactly equivalent to:
my_function = decorator_a(decorator_b(decorator_c(my_function)))

# When my_function() is called:
# 1. decorator_a's wrapper runs first (outermost)
# 2. decorator_b's wrapper runs second
# 3. decorator_c's wrapper runs third (innermost, closest to the real function)
# 4. The real my_function body runs
# 5. Unwinding: decorator_c → decorator_b → decorator_a
python
# Practical example: order matters for @timer and @retry
# If timer is outermost, it measures total time including retry wait periods.
# If retry is outermost, it measures only the final successful call.

@timer          # measures: total time across all retry attempts + sleep
@retry(max_attempts=3, delay=1.0)
def sync_with_partner_api(partner_id: int) -> dict:
    ...

# vs.

@retry(max_attempts=3, delay=1.0)  # measures: only the final successful call
@timer
def sync_with_partner_api(partner_id: int) -> dict:
    ...

# Usually you want @timer outermost — it tells you the real wall-clock cost of the operation.
# Think about what "calling this function" means to the caller, then wrap in that order.
Suggerimento sul traceback: Quando una funzione decorata lancia un'eccezione, il traceback mostrerà ogni wrapper nello stack di chiamate. Se hai usato @functools.wraps correttamente, i nomi rifletteranno le funzioni originali. Se vedi un mare di frame wrapper, qualcuno ha dimenticato @functools.wraps. Il primer sui decorator di Real Python ha una buona guida su come debuggare decorator impilati.

Conclusioni

Il modello mentale da portare con sé: @decorator è fn = decorator(fn). Tutto il resto — decorator con argomenti, decorator basati su classi, decorator impilati — è una variazione su quell'unica sostituzione. Usa @functools.wraps su ogni wrapper interno, inoltra sempre *args, **kwargs alla funzione avvolta, e restituisci il suo risultato. I decorator con argomenti hanno bisogno di tre livelli di nesting: la funzione argomento, il decorator e il wrapper. I decorator basati su classi sono la scelta giusta quando il tuo decorator deve mantenere stato tra le chiamate.

Per ulteriori letture: la voce del glossario Python sui decorator è breve ma precisa. La proposta originale del decorator, PEP 318, vale la lettura per il contesto su perché è stata scelta la sintassi @ rispetto alle alternative. Se stai usando decorator in una codebase che fa anche molto processing di dati — leggere file, trasformare record — i pattern qui si abbinano naturalmente a quanto trattato in Python File Handling. E se stai usando decorator per trasformare collezioni o costruire strutture di lookup, Python List Comprehensions copre il lato trasformazione dei dati di quel quadro. Se le tue funzioni decorate restituiscono output JSON e vuoi ispezionarlo rapidamente, il JSON Formatter su questo sito è comodo per questo.