Closure in Python
Scopri come funzionano le closure in Python: funzioni annidate, variabili catturate, nonlocal, errori comuni e casi d'uso reali con esempi eseguibili.
Una closure è una funzione annidata che ricorda le variabili dello scope esterno in cui è stata definita, anche dopo che quella funzione esterna ha restituito il controllo. Le closure ti permettono di associare uno stato privato a una funzione senza usare una classe, rendendole uno degli strumenti più eleganti di Python per costruire callback, factory e helper con stato.
Questa pagina spiega come funzionano le closure, le tre condizioni che richiedono, le insidie più comuni e i casi d'uso pratici.
Cos'è una Closure?
Quando Python esegue una funzione, crea uno scope locale che scompare non appena la funzione restituisce il controllo. Di norma, tutte le variabili definite lì spariscono. Una closure è l'eccezione: se una funzione interna fa riferimento a una variabile di una funzione esterna, Python mantiene quella variabile in vita in uno speciale cell object, e la funzione interna porta con sé un riferimento a quei cell ovunque vada.
La closure più semplice è una function factory — una funzione che costruisce e restituisce un'altra funzione:
def make_multiplier(factor):
def multiply(n):
return n * factor # 'factor' is captured from the enclosing scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(10)) # 20Ogni chiamata a make_multiplier crea una nuova closure con la propria copia indipendente di factor. double e triple sono completamente indipendenti anche se sono state create dalla stessa funzione.
Tre Condizioni per una Closure
Una funzione è una closure quando tutte e tre le seguenti condizioni sono vere:
- Esiste una funzione annidata — una funzione definita all'interno di un'altra funzione.
- La funzione annidata fa riferimento a una variabile dello scope esterno — quella variabile si chiama variabile libera.
- La funzione esterna restituisce la funzione annidata (o la passa da qualche altra parte).
def outer():
message = 'Hello from outer' # free variable
def inner():
print(message) # inner references it
return inner # outer returns inner
greet = outer()
greet() # Hello from outerDopo che outer() restituisce il controllo, il suo frame locale è sparito — ma message sopravvive all'interno di greet.__closure__.
Ispezionare una Closure
Python espone le celle della closure tramite l'attributo __closure__:
def make_adder(n):
def add(x):
return x + n
return add
add5 = make_adder(5)
print(add5(3)) # 8
print(add5.__closure__) # (<cell at 0x...>,)
print(add5.__closure__[0].cell_contents) # 5__closure__ è una tupla di cell object — uno per ogni variabile catturata. Se una funzione non è una closure, __closure__ vale None.
Modificare le Variabili Catturate con nonlocal
Per impostazione predefinita, puoi leggere una variabile catturata ma non riassegnarla. Tentare di assegnarle un valore crea una nuova variabile locale, il che di solito non è quello che vuoi. Usa la parola chiave nonlocal per indicare a Python che intendi la variabile dello scope esterno:
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count # rebind the enclosing 'count', not a new local
count += step
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter(5)) # 7
counter2 = make_counter(10)
print(counter2()) # 11
print(counter()) # 8 — counter is unaffectedOgni chiamata a make_counter produce una cella count indipendente. counter e counter2 non condividono lo stato.
Per un'analisi più approfondita di come Python determina a quale scope appartiene una variabile, consulta Python Scope.
Errore Comune: Closure nei Cicli
Un errore classico consiste nel creare closure all'interno di un ciclo aspettandosi che ciascuna catturi il valore corrente della variabile di ciclo:
# Wrong — all functions capture the same 'i' cell
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2] — not [0, 1, 2]Tutte e tre le lambda condividono una cella che contiene la variabile di ciclo i. Al momento in cui vengono chiamate, i ha raggiunto il suo valore finale 2.
Soluzione 1: Argomento predefinito (cattura per valore)
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # default arg is evaluated immediately
print([f() for f in funcs]) # [0, 1, 2]Soluzione 2: Funzione factory
def make_func(i):
def f():
return i
return f
funcs = [make_func(i) for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]La funzione factory crea un nuovo scope — e quindi una nuova cella — per ogni iterazione. Questo è l'approccio più esplicito e leggibile.
Casi d'Uso Pratici
Applicazione Parziale
Le closure sono un'alternativa leggera a functools.partial quando hai bisogno di una versione pre-configurata di una funzione:
def make_power(exponent):
def power(base):
return base ** exponent
return power
square = make_power(2)
cube = make_power(3)
print(square(4)) # 16
print(cube(3)) # 27Memoizzazione Semplice
Una closure può contenere un dizionario cache che persiste tra le chiamate:
def make_memoized(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@make_memoized
def slow_square(n):
return n * n
print(slow_square(4)) # 16
print(slow_square(4)) # 16 (served from cache)
print(slow_square(7)) # 49Questo pattern è esattamente il funzionamento dei decoratori Python sotto il cofano — un decoratore è semplicemente una closure che racchiude un'altra funzione.
Configurazione di Callback
Le closure sono utili per costruire callback che necessitano di un po' di contesto incorporato:
def make_logger(prefix):
def log(message):
print(f'[{prefix}] {message}')
return log
info = make_logger('INFO')
error = make_logger('ERROR')
info('Server started') # [INFO] Server started
error('Disk full') # [ERROR] Disk fullClosure vs. Classi
Una closure e una classe con un singolo metodo risolvono spesso lo stesso problema. Scegli in base alla complessità:
| Situazione | Preferisci |
|---|---|
| Un solo stato, un solo comportamento | Closure |
| Più metodi o attributi pubblici | Classe |
| Deve essere serializzata (es. pickle) | Classe |
| Passare un callback a un'altra funzione | Closure |
# Class approach
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self, step=1):
self.count += step
return self.count
# Closure approach
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count
count += step
return count
return incrementEntrambi producono un comportamento identico. La closure è più concisa; la classe è più scopribile ed estensibile.
Conclusione
Le closure permettono a una funzione annidata di portare il proprio stato privato ricordando le variabili dello scope in cui è stata definita. I punti chiave sono:
- Una closure richiede una funzione annidata, una variabile libera e la funzione interna che viene restituita o passata.
- Usa
nonlocalquando hai bisogno di riassegnare (non solo leggere) una variabile catturata. - Evita l'insidia della variabile di ciclo: usa una funzione factory o un argomento predefinito per acquisire il valore a ogni iterazione.
- Le closure sono il fondamento dei decoratori e sono strettamente correlate alle regole di scope di Python.