W3docs

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))  # 20

Ogni 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:

  1. Esiste una funzione annidata — una funzione definita all'interno di un'altra funzione.
  2. La funzione annidata fa riferimento a una variabile dello scope esterno — quella variabile si chiama variabile libera.
  3. 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 outer

Dopo 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 unaffected

Ogni 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))    # 27

Memoizzazione 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))   # 49

Questo 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 full

Closure vs. Classi

Una closure e una classe con un singolo metodo risolvono spesso lo stesso problema. Scegli in base alla complessità:

SituazionePreferisci
Un solo stato, un solo comportamentoClosure
Più metodi o attributi pubbliciClasse
Deve essere serializzata (es. pickle)Classe
Passare un callback a un'altra funzioneClosure
# 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 increment

Entrambi 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 nonlocal quando 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.

Pratica

Pratica
Which keyword allows an inner function to rebind a variable from its enclosing scope?
Which keyword allows an inner function to rebind a variable from its enclosing scope?
Pratica
What does the __closure__ attribute return when a function is NOT a closure?
What does the __closure__ attribute return when a function is NOT a closure?
Was this page helpful?