W3docs

Decoratori Python

Scopri come funzionano i decoratori Python: come scriverli, preservare i metadati con functools.wraps, combinarli e casi d'uso pratici.

Un decoratore è una funzione che avvolge un'altra funzione per estenderne o modificarne il comportamento senza cambiarne il codice sorgente. I decoratori sono una delle funzionalità più potenti e idiomatiche di Python — sono il motore dietro @staticmethod, @classmethod, @property, @functools.lru_cache e molti pattern popolari nei framework web.

Questa pagina spiega come funzionano i decoratori, come scriverne uno da zero, come passare argomenti ai decoratori, come combinarli e quando ciascun pattern è più utile.

Come Funzionano i Decoratori

Un decoratore è semplicemente una funzione che accetta un'altra funzione come argomento e restituisce una nuova funzione. Python fornisce la sintassi @ come abbreviazione per applicarne uno:

@shout
def greet(name):
    return f"hello, {name}"

Questo è esattamente equivalente a:

def greet(name):
    return f"hello, {name}"

greet = shout(greet)

La riga @shout dice a Python: dopo aver definito greet, passala immediatamente a shout e riassegna il nome greet a qualunque cosa shout restituisca. Da quel momento in poi, ogni chiamata a greet(...) passa prima per la logica di shout.

Scrivere il Primo Decoratore

Un decoratore di solito definisce una funzione wrapper interna che chiama la funzione originale e aggiunge comportamento extra intorno ad essa:

def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@shout
def greet(name):
    return f"hello, {name}"

print(greet("world"))   # HELLO, WORLD
print(greet("python"))  # HELLO, PYTHON

wrapper accetta *args e **kwargs in modo da poter inoltrare qualsiasi combinazione di argomenti a func invariata. Questo rende il decoratore compatibile con qualsiasi funzione indipendentemente dalla sua firma — una buona abitudine fin dall'inizio.

Perché il Wrapper Deve Restituire la Funzione Interna

shout termina con return wrapper, non return wrapper(). Questo è intenzionale: shout sta costruendo un nuovo callable, non lo sta ancora chiamando. Se per errore si scrivesse return wrapper(), il decoratore verrebbe eseguito immediatamente al momento della decorazione e greet sarebbe associato al valore restituito da wrapper — una stringa — anziché al callable stesso.

Preservare i Metadati con functools.wraps

Ogni funzione Python porta con sé dei metadati: __name__, __doc__, __module__ e altro. Senza un'attenzione particolare, un decoratore sostituisce la funzione originale con wrapper, perdendo tutto ciò:

def shout(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

@shout
def greet(name):
    """Say hello to name."""
    return f"hello, {name}"

print(greet.__name__)  # wrapper  — wrong
print(greet.__doc__)   # None     — lost

Si risolve applicando @functools.wraps(func) al wrapper. Copia i metadati della funzione originale su wrapper:

import functools

def shout(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@shout
def greet(name):
    """Say hello to name."""
    return f"hello, {name}"

print(greet("world"))  # HELLO, WORLD
print(greet.__name__)  # greet
print(greet.__doc__)   # Say hello to name.

Usa sempre @functools.wraps in qualsiasi decoratore che scrivi. Senza di esso, gli strumenti di debug, i generatori di documentazione e i framework di test vedono il nome della funzione sbagliato. L'unica eccezione è quando si vuole intenzionalmente nascondere l'identità originale.

Esempi Pratici di Decoratori

Logger

Registra ogni chiamata a una funzione con i suoi argomenti e il valore restituito:

import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result!r}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(3, 5)
# Calling add with args=(3, 5) kwargs={}
# add returned 8

Timer

Misura il tempo impiegato da una funzione per essere eseguita:

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__} took {elapsed:.6f}s")
        return result
    return wrapper

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

total = slow_sum(1_000_000)
print(total)  # slow_sum took 0.01xxs  then  499999500000

time.perf_counter() è la scelta giusta qui perché ha la risoluzione più alta disponibile per misurazioni di breve durata.

Memoization (Cache)

Memorizza nella cache il valore restituito per ogni insieme unico di argomenti, così la funzione non viene mai calcolata due volte per lo stesso input:

import functools

def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # 55
print(fibonacci(30))  # 832040

Per il codice in produzione, preferisci il built-in @functools.lru_cache o @functools.cache (Python 3.9+), che gestiscono i casi limite, la thread safety e i limiti di dimensione della cache. La versione fatta a mano è utile per comprendere il pattern.

Controllo degli Accessi

Proteggi una funzione in modo che possa essere eseguita solo quando una condizione è soddisfatta:

import functools

def require_auth(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_authenticated"):
            raise PermissionError("Authentication required.")
        return func(user, *args, **kwargs)
    return wrapper

@require_auth
def get_dashboard(user):
    return f"Welcome, {user['name']}!"

guest = {"name": "Guest", "is_authenticated": False}
admin = {"name": "Admin", "is_authenticated": True}

try:
    print(get_dashboard(guest))
except PermissionError as e:
    print(e)                      # Authentication required.

print(get_dashboard(admin))       # Welcome, Admin!

Decoratori con Argomenti

A volte è necessario configurare un decoratore al momento della decorazione — ad esempio, per ripetere una funzione un numero variabile di volte. I decoratori semplici non possono accettare argomenti extra direttamente perché Python passa la funzione, non gli argomenti. La soluzione è una decorator factory: una funzione che accetta la configurazione e restituisce un decoratore:

import functools

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

@repeat(3)
def say(message):
    print(message)

say("hello")
# hello
# hello
# hello

Leggendo dall'esterno verso l'interno: @repeat(3) chiama prima repeat(3), che restituisce decorator. Python applica quindi decorator a say, che restituisce wrapper. Quindi say finisce per puntare a wrapper — stesso pattern di prima, con il livello extra solo per portare n nello scope.

L'annidamento può sembrare scoraggiante all'inizio. Una scorciatoia mentale: la funzione più esterna contiene la configurazione, quella intermedia contiene la funzione decorata e quella più interna contiene la chiamata intercettata.

Combinare Più Decoratori

È possibile applicare più decoratori a una singola funzione sovrapponendo le righe @. Python li applica dal basso verso l'alto — il decoratore più vicino al def viene applicato per primo:

import functools

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<b>" + func(*args, **kwargs) + "</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<i>" + func(*args, **kwargs) + "</i>"
    return wrapper

@bold
@italic
def greet(name):
    return f"Hello, {name}"

print(greet("Alice"))  # <b><i>Hello, Alice</i></b>

Equivalente a greet = bold(italic(greet)). italic avvolge greet per primo, poi bold avvolge il risultato. L'output mostra che italic viene eseguito più vicino alla stringa grezza e bold avvolge l'esterno.

Decoratori Basati su Classi

Anche una classe può essere un decoratore — qualsiasi oggetto con un metodo __call__ è callable. I decoratori basati su classi sono utili quando il decoratore stesso ha bisogno di mantenere uno stato tra le chiamate:

import functools

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

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call #{self.count} to {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()
say_hello()
print(say_hello.count)  # 2

functools.update_wrapper(self, func) svolge lo stesso compito di @functools.wraps — copia i metadati della funzione originale sull'istanza. Dopo la decorazione, say_hello è un'istanza di CountCalls, quindi say_hello.count è un normale accesso a un attributo.

Quando scegliere una classe rispetto a un decoratore funzione:

  • Hai bisogno di uno stato persistente (count, cache, flag).
  • Il decoratore ha più metodi o logica ausiliaria.
  • Hai bisogno che l'oggetto decorato sia introspezionabile come un tipo specifico.

Errori Comuni con i Decoratori

Dimenticare di chiamare la funzione decorata

Un errore comune all'inizio è restituire il wrapper ma dimenticare di chiamare func al suo interno:

def broken(func):
    def wrapper(*args, **kwargs):
        print("before")
        # forgot to call func!
    return wrapper

La funzione decorata restituisce silenziosamente None ogni volta. Assicurati sempre che wrapper chiami func(*args, **kwargs) e restituisca il suo risultato.

Decorare al livello sbagliato

Quando si usano decoratori parametrizzati, dimenticare la chiamata esterna è un errore frequente:

# Wrong — 'repeat' receives the function, not a count
@repeat      # should be @repeat(3)
def say(msg):
    print(msg)

Questo passa say a repeat dove ci si aspetta n, causando un TypeError quando si chiama say.

L'ordine dei decoratori è importante

Con decoratori combinati l'ordine cambia il comportamento. @timer seguito da @log_calls sulla stessa funzione misureranno il tempo della versione già registrata, mentre il contrario registrerà la versione già cronometrata. Pensa bene a cosa vuoi che ogni livello veda.

Relazione con le Closure

La funzione wrapper di un decoratore è una closure — cattura func dallo scope circostante e la mantiene in vita anche dopo che la funzione decoratore esterna ha restituito il controllo. Comprendere le closure rende ovvio il funzionamento interno dei decoratori: l'oggetto cell che contiene func è esattamente ciò che permette a wrapper di chiamare la funzione originale molto dopo che shout(greet) è stato completato.

Per la sintassi *args e **kwargs usata all'interno dei wrapper, consulta il capitolo dedicato. Per le espressioni lambda che si abbinano bene ai decoratori nei pattern di ordine superiore, consulta il capitolo sul lambda.

Riferimento Rapido

PatternQuando usarlo
wrapper di baseAggiungere comportamento prima/dopo una funzione
@functools.wrapsSempre — preserva __name__, __doc__
Decorator factory (3 livelli)Necessità di configurare il decoratore
Decoratori combinatiComporre più comportamenti indipendenti
Decoratore basato su classeNecessità di stato persistente tra le chiamate
@functools.lru_cacheMemoizzare funzioni pure (built-in, pronto per la produzione)

Esercizi

Pratica
What does @functools.wraps(func) do inside a decorator?
What does @functools.wraps(func) do inside a decorator?
Pratica
Given @bold applied above @italic on a function, which decorator is applied first?
Given @bold applied above @italic on a function, which decorator is applied first?
Pratica
What is a decorator factory?
What is a decorator factory?
Was this page helpful?