ProgrammazioneSviluppatore Backend

Spiega come funzionano le closures in Python, in che modo si differenziano dalle funzioni normali e quale sia il loro utilizzo pratico?

Supera i colloqui con l'assistente IA Hintsage

Risposta.

Storia della domanda

Il termine "closure" è stato preso dalla programmazione funzionale ed è presente in Python sin dall'inizio. Le closures consentono a una funzione di ricordare l'ambiente in cui è stata creata, anche se vengono chiamate al di fuori di tale ambiente. Questo concetto fornisce flessibilità e consente di implementare molti schemi, comprese le fabbriche di funzioni e i calcoli pigri.

Problema

In Python, le funzioni sono oggetti di prima classe. A volte è necessario che una funzione interna utilizzi variabili dall'ambito della funzione esterna, anche dopo che quest'ultima è terminata. Il normale scoping lessicale non garantisce ciò al momento del ritorno della funzione. Se una funzione si riferisce alle variabili del suo ambiente di creazione, si verifica una closure.

Soluzione

Una closure si verifica se una funzione interna fa riferimento a variabili definite nell'esterno e quella esterna restituisce l'interna al di fuori. Questo è spesso utilizzato per creare fabbriche di funzioni, incapsulando lo stato senza classi e costruendo funzioni con parametri "in-loco".

Esempio di codice:

def make_multiplier(factor): def multiplier(x): return x * factor return multiplier mul2 = make_multiplier(2) mul3 = make_multiplier(3) print(mul2(10)) # 20 print(mul3(10)) # 30

Caratteristiche chiave:

  • Una closure memorizza i valori delle variabili dell'ambiente, anche se la funzione esterna ha già terminato.
  • Lo stato nella funzione interna è sostanzialmente privato e non può essere modificato direttamente dall'esterno.
  • Per modificare una variabile non-immutable nella funzione esterna all'interno di una closure, si utilizza la parola chiave nonlocal.

Domande trabocchetto.

Può una closure mantenere uno stato modificabile tra le chiamate, se la variabile viene modificata all'interno della funzione interna?

Sì, se all'interno della funzione interna viene utilizzata la parola chiave nonlocal. Senza nonlocal, l'assegnazione crea una nuova variabile locale, senza modificare l'esterno.

def counter(): count = 0 def inc(): nonlocal count count += 1 return count return inc c = counter() print(c()) # 1 print(c()) # 2

È possibile implementare variabili private in Python usando le closures invece delle classi?

Sì, le closure offrono una semplice implementazione di variabili "private" che non sono accessibili dall'esterno, a meno che non vengano forniti getter/setter nella funzione interna.

Le closures si applicano solo alle funzioni? È possibile organizzare una closure con le lambda in Python?

Sì, una closure può essere formata anche con le espressioni lambda poiché sono simili a def per il binding delle variabili lessicali.

def make_power(n): return lambda x: x ** n square = make_power(2) cube = make_power(3) print(square(4)) # 16 print(cube(2)) # 8

Errori comuni e anti-pattern

  • Aspettarsi che la closure modifichi automaticamente la variabile esterna quando viene modificata all'interno senza nonlocal.
  • Catturare oggetti modificabili all'interno di una closure e mutarli, senza rendersi conto dei problemi di debug.
  • Usare un ciclo per creare funzioni in una closure senza il giusto binding delle variabili (trappola "tutte le funzioni vedono l'ultimo valore della variabile").

Esempio dalla vita reale

Caso negativo

Una fabbrica di funzioni che crea gestori in un ciclo utilizza una variabile di ciclo all'interno della closure:

handlers = [] for i in range(3): def handler(x): return x + i handlers.append(handler) print([h(10) for h in handlers]) # [12, 12, 12]

Vantaggi:

  • Semplice, poco codice.

Svantaggi:

  • Tutti i gestori fanno riferimento alla stessa variabile i, il suo ultimo valore è 2 — comportamento inaspettato per la maggior parte.

Caso positivo

Utilizzato un argomento di default per "fissare" il valore:

handlers = [] for i in range(3): def handler(x, j=i): return x + j handlers.append(handler) print([h(10) for h in handlers]) # [10, 11, 12]

Vantaggi:

  • Binding del valore necessario.
  • Comportamento prevedibile.

Svantaggi:

  • È necessario ricordarsi di questo dettaglio e correggere manualmente la closure, il che complica la manutenibilità del codice.