Perché le funzioni nidificate di Python non sono chiamate chiusure?


249

Ho visto e usato funzioni nidificate in Python e corrispondono alla definizione di chiusura. Quindi perché vengono chiamati nested functionsinvece di closures?

Le funzioni nidificate non sono chiusure perché non sono utilizzate dal mondo esterno?

AGGIORNAMENTO: stavo leggendo delle chiusure e mi ha fatto pensare a questo concetto rispetto a Python. Ho cercato e trovato l'articolo citato da qualcuno in un commento qui sotto, ma non riuscivo a comprendere completamente la spiegazione in quell'articolo, quindi è per questo che sto ponendo questa domanda.


8
È interessante notare che alcuni googling mi hanno trovato questo, datato dicembre 2006: effbot.org/zone/closure.htm . Non sono sicuro: i "duplicati esterni" sono disapprovati su SO?
hbw,

Risposte:


394

Una chiusura si verifica quando una funzione ha accesso a una variabile locale da un ambito che ha terminato la sua esecuzione.

def make_printer(msg):
    def printer():
        print msg
    return printer

printer = make_printer('Foo!')
printer()

Quando make_printerviene chiamato, un nuovo frame viene messo nello stack con il codice compilato per la printerfunzione come costante e il valore di msgcome locale. Quindi crea e restituisce la funzione. Poiché la funzione fa printerriferimento alla msgvariabile, viene mantenuta attiva dopo il make_printerritorno della funzione.

Quindi, se le tue funzioni nidificate no

  1. accedere a variabili locali per ambiti racchiusi,
  2. farlo quando vengono eseguiti al di fuori di tale ambito,

quindi non sono chiusure.

Ecco un esempio di una funzione nidificata che non è una chiusura.

def make_printer(msg):
    def printer(msg=msg):
        print msg
    return printer

printer = make_printer("Foo!")
printer()  #Output: Foo!

Qui, stiamo associando il valore al valore predefinito di un parametro. Ciò si verifica quando printerviene creata la funzione e quindi non è necessario mantenere alcun riferimento al valore di msgesterno a printerdopo i make_printerresi. msgè solo una normale variabile locale della funzione printerin questo contesto.


2
La tua risposta è molto migliore della mia, fai un buon punto, ma se andremo dalle più rigorose definizioni di programmazione funzionale, i tuoi esempi sono anche funzioni? È passato un po 'di tempo e non ricordo se una rigida programmazione funzionale consenta funzioni che non restituiscono valori. Il punto è controverso, se si considera che il valore restituito è Nessuno, ma questo è un altro argomento.
mikerobi,

6
@mikerobi, non sono sicuro che dobbiamo prendere in considerazione la programmazione funzionale poiché python non è in realtà un linguaggio funzionale sebbene possa certamente essere usato come tale. Ma no, le funzioni interne non sono funzioni in quel senso poiché il loro punto è creare effetti collaterali. È facile creare una funzione che illustri anche i punti, però
aaronasterling

31
@mikerobi: la chiusura di un BLOB di codice dipende dal fatto che si chiuda o meno sul suo ambiente, non da come lo si chiama. Potrebbe essere una routine, funzione, procedura, metodo, blocco, subroutine, qualunque cosa. In Ruby, i metodi non possono essere chiusure, solo i blocchi possono. In Java, i metodi non possono essere chiusure, ma le classi possono. Ciò non li rende meno di una chiusura. (Sebbene il fatto che si chiudano solo su alcune variabili e non possano modificarle, le rende quasi inutili.) Si potrebbe sostenere che un metodo è solo una procedura chiusa self. (In JavaScript / Python è quasi vero.)
Jörg W Mittag,

3
@ JörgWMittag Si prega di definire "chiude".
Evgeni Sergeev,

4
@EvgeniSergeev "chiude", ovvero fa riferimento a "una variabile locale [diciamo, i] da un ambito racchiuso". si riferisce, cioè può ispezionare (o modificare) iil valore, anche se / quando tale ambito "ha terminato la sua esecuzione", ovvero l'esecuzione di un programma è andata avanti ad altre parti del codice. Il blocco in cui iè definito non è più, ma le funzioni a cui si fa iancora riferimento possono farlo. Questo è comunemente descritto come "chiusura sopra la variabile i". Per non gestire le variabili specifiche, può essere implementato come chiusura su tutto il frame di ambiente in cui è definita quella variabile.
Will Ness,

103

Aaronasterling ha già risposto alla domanda

Tuttavia, qualcuno potrebbe essere interessato a come le variabili sono memorizzate sotto il cofano.

Prima di venire allo snippet:

Le chiusure sono funzioni che ereditano le variabili dal loro ambiente di chiusura. Quando si passa un callback di funzione come argomento a un'altra funzione che eseguirà I / O, questa funzione di callback verrà invocata in seguito e questa funzione - quasi magicamente - ricorderà il contesto in cui è stata dichiarata, insieme a tutte le variabili disponibili in quel contesto.

  • Se una funzione non utilizza variabili libere non costituisce una chiusura.

  • Se esiste un altro livello interno che utilizza variabili libere, tutti i livelli precedenti salvano l'ambiente lessicale (esempio alla fine)

  • gli attributi della funzione func_closurein python <3.X o __closure__in python> 3.X salvano le variabili libere.

  • Ogni funzione in Python ha questi attributi di chiusura, ma non salva alcun contenuto se non ci sono variabili libere.

esempio: di attributi di chiusura ma nessun contenuto all'interno in quanto non esiste una variabile libera.

>>> def foo():
...     def fii():
...         pass
...     return fii
...
>>> f = foo()
>>> f.func_closure
>>> 'func_closure' in dir(f)
True
>>>

NB: LA VARIABILE GRATUITA DEVE CREARE UNA CHIUSURA.

Spiegherò usando lo stesso frammento di cui sopra:

>>> def make_printer(msg):
...     def printer():
...         print msg
...     return printer
...
>>> printer = make_printer('Foo!')
>>> printer()  #Output: Foo!

E tutte le funzioni di Python hanno un attributo di chiusura, quindi esaminiamo le variabili di chiusura associate a una funzione di chiusura.

Ecco l'attributo func_closureper la funzioneprinter

>>> 'func_closure' in dir(printer)
True
>>> printer.func_closure
(<cell at 0x108154c90: str object at 0x108151de0>,)
>>>

L' closureattributo restituisce una tupla di oggetti cella che contengono dettagli delle variabili definite nell'ambito compreso.

Il primo elemento in func_closure che potrebbe essere None o una tupla di celle che contengono associazioni per le variabili libere della funzione ed è di sola lettura.

>>> dir(printer.func_closure[0])
['__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__getattribute__',
 '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
 '__setattr__',  '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
>>>

Qui nell'output sopra puoi vedere cell_contents, vediamo cosa memorizza:

>>> printer.func_closure[0].cell_contents
'Foo!'    
>>> type(printer.func_closure[0].cell_contents)
<type 'str'>
>>>

Quindi, quando abbiamo chiamato la funzione printer(), accede al valore memorizzato all'interno di cell_contents. Ecco come abbiamo ottenuto l'output come "Foo!"

Spiegherò ancora usando lo snippet sopra con alcune modifiche:

 >>> def make_printer(msg):
 ...     def printer():
 ...         pass
 ...     return printer
 ...
 >>> printer = make_printer('Foo!')
 >>> printer.func_closure
 >>>

Nel frammento sopra, non stampo msg all'interno della funzione stampante, quindi non crea alcuna variabile libera. Poiché non esiste una variabile libera, non ci sarà contenuto all'interno della chiusura. Questo è esattamente ciò che vediamo sopra.

Ora spiegherò un altro frammento diverso per cancellare tutto Free Variablecon Closure:

>>> def outer(x):
...     def intermediate(y):
...         free = 'free'
...         def inner(z):
...             return '%s %s %s %s' %  (x, y, free, z)
...         return inner
...     return intermediate
...
>>> outer('I')('am')('variable')
'I am free variable'
>>>
>>> inter = outer('I')
>>> inter.func_closure
(<cell at 0x10c989130: str object at 0x10c831b98>,)
>>> inter.func_closure[0].cell_contents
'I'
>>> inn = inter('am')

Quindi, vediamo che una func_closureproprietà è una tupla di celle di chiusura , possiamo riferirle esplicitamente e il loro contenuto - una cella ha proprietà "cell_contents"

>>> inn.func_closure
(<cell at 0x10c9807c0: str object at 0x10c9b0990>, 
 <cell at 0x10c980f68: str object at   0x10c9eaf30>, 
 <cell at 0x10c989130: str object at 0x10c831b98>)
>>> for i in inn.func_closure:
...     print i.cell_contents
...
free
am 
I
>>>

Qui quando abbiamo chiamato inn, farà riferimento a tutte le variabili libere di salvataggio in modo da ottenereI am free variable

>>> inn('variable')
'I am free variable'
>>>

9
In Python 3, func_closureora viene chiamato __closure__, in modo simile ai vari altri func_*attributi.
PVC

3
Inoltre __closure_è disponibile in Python 2.6+ per la compatibilità con Python 3.
Pierre,

La chiusura si riferisce al record che memorizza le variabili chiuse, associate all'oggetto funzione. Non è la funzione stessa. In Python, è l' __closure__oggetto che è la chiusura.
Martijn Pieters

Grazie @MartijnPieters per il tuo chiarimento.
James Sapam,

71

Python ha un debole supporto per la chiusura. Per capire cosa intendo, prendere il seguente esempio di contatore che utilizza la chiusura con JavaScript:

function initCounter(){
    var x = 0;
    function counter  () {
        x += 1;
        console.log(x);
    };
    return counter;
}

count = initCounter();

count(); //Prints 1
count(); //Prints 2
count(); //Prints 3

La chiusura è piuttosto elegante poiché offre a funzioni scritte in questo modo la possibilità di avere "memoria interna". A partire da Python 2.7 questo non è possibile. Se provi

def initCounter():
    x = 0;
    def counter ():
        x += 1 ##Error, x not defined
        print x
    return counter

count = initCounter();

count(); ##Error
count();
count();

Verrà visualizzato un errore che indica che x non è definito. Ma come può essere se è stato dimostrato da altri che puoi stamparlo? Ciò è dovuto al modo in cui Python gestisce l'ambito variabile delle funzioni. Mentre la funzione interna può leggere le variabili della funzione esterna, non può scriverle .

Questo è davvero un peccato. Ma con la sola chiusura di sola lettura puoi almeno implementare il modello di decoratore di funzioni per il quale Python offre zucchero sintattico.

Aggiornare

Come è stato sottolineato, ci sono modi per gestire i limiti dell'ambito di Python e ne esporrò alcuni.

1. Utilizzare la globalparola chiave (in generale non consigliata).

2. In Python 3.x, usa la nonlocalparola chiave (suggerita da @unutbu e @leewz)

3. Definire una semplice classe modificabileObject

class Object(object):
    pass

e crea un Object scopeinterno initCounterper memorizzare le variabili

def initCounter ():
    scope = Object()
    scope.x = 0
    def counter():
        scope.x += 1
        print scope.x

    return counter

Poiché scopeè in realtà solo un riferimento, le azioni intraprese con i suoi campi non si modificano realmente scope, quindi non si verificano errori.

4. Un modo alternativo, come sottolineato da @unutbu, sarebbe quello di definire ogni variabile come un array ( x = [0]) e modificarne il primo elemento ( x[0] += 1). Ancora una volta non si verifica alcun errore perché xnon viene modificato di per sé.

5. Come suggerito da @raxacoricofallapatorius, potresti fare xuna proprietà dicounter

def initCounter ():

    def counter():
        counter.x += 1
        print counter.x

    counter.x = 0
    return counter

27
Ci sono modi per aggirare questo. In Python2, è possibile creare x = [0]nell'ambito esterno e utilizzarlo x[0] += 1nell'ambito interno. In Python3, è possibile mantenere il codice così com'è e utilizzare la parola chiave non locale .
unutbu,

"Mentre la funzione interna può leggere le variabili della funzione esterna, non può scriverle." - Questo non è preciso secondo il commento di unutbu. Il problema è che quando Python incontra qualcosa come x = ..., x viene interpretato come una variabile locale, che ovviamente non è ancora definita in quel punto. OTOH, se x è un oggetto mutabile con un metodo mutabile, può essere modificato bene, ad esempio se x è un oggetto che supporta il metodo inc () che si muta da solo, x.inc () funzionerà senza intoppi.
Thanh DK,

@ThanhDK Questo non significa che non puoi scrivere nella variabile? Quando usi chiamare un metodo da un oggetto mutabile, stai semplicemente dicendo che si modifica da solo, non stai effettivamente modificando la variabile (che contiene semplicemente un riferimento all'oggetto). In altre parole, il riferimento a cui xpunta la variabile rimane esattamente lo stesso anche se chiami inc()o qualsiasi altra cosa e non hai effettivamente scritto sulla variabile.
user193130,

4
C'è un'altra opzione, strettamente migliore di # 2, imv, di fare xuna proprietà dicounter .
orome,

9
Python 3 ha la nonlocalparola chiave, che è simile globalma per le variabili di una funzione esterna. Ciò consentirà a una funzione interna di associare un nome alle sue funzioni esterne. Penso che "associare al nome" sia più preciso di "modifica la variabile".
Leewz,

16

Python 2 non aveva chiusure: aveva soluzioni alternative che ricordavano chiusure.

Ci sono molti esempi nelle risposte già fornite: copiare le variabili nella funzione interna, modificare un oggetto sulla funzione interna, ecc.

In Python 3, il supporto è più esplicito e succinto:

def closure():
    count = 0
    def inner():
        nonlocal count
        count += 1
        print(count)
    return inner

Uso:

start = closure()
start() # prints 1
start() # prints 2
start() # prints 3

La nonlocalparola chiave associa la funzione interna alla variabile esterna menzionata esplicitamente, racchiudendola in effetti. Quindi più esplicitamente una "chiusura".


1
Interessante, per riferimento: docs.python.org/3/reference/… . Non so perché non sia facile trovare maggiori informazioni sulle chiusure (e come ci si potrebbe aspettare che si comportino, provenienti da JS) nella documentazione di Python3?
user3773048

9

Ho avuto una situazione in cui avevo bisogno di uno spazio dei nomi separato ma persistente. Ho usato le lezioni. Altrimenti no. I nomi separati ma persistenti sono chiusure.

>>> class f2:
...     def __init__(self):
...         self.a = 0
...     def __call__(self, arg):
...         self.a += arg
...         return(self.a)
...
>>> f=f2()
>>> f(2)
2
>>> f(2)
4
>>> f(4)
8
>>> f(8)
16

# **OR**
>>> f=f2() # **re-initialize**
>>> f(f(f(f(2)))) # **nested**
16

# handy in list comprehensions to accumulate values
>>> [f(i) for f in [f2()] for i in [2,2,4,8]][-1] 
16

6
def nested1(num1): 
    print "nested1 has",num1
    def nested2(num2):
        print "nested2 has",num2,"and it can reach to",num1
        return num1+num2    #num1 referenced for reading here
    return nested2

dà:

In [17]: my_func=nested1(8)
nested1 has 8

In [21]: my_func(5)
nested2 has 5 and it can reach to 8
Out[21]: 13

Questo è un esempio di cos'è una chiusura e di come può essere utilizzata.


0

Vorrei offrire un altro semplice confronto tra python e l'esempio JS, se questo aiuta a rendere le cose più chiare.

JS:

function make () {
  var cl = 1;
  function gett () {
    console.log(cl);
  }
  function sett (val) {
    cl = val;
  }
  return [gett, sett]
}

ed eseguendo:

a = make(); g = a[0]; s = a[1];
s(2); g(); // 2
s(3); g(); // 3

Pitone:

def make (): 
  cl = 1
  def gett ():
    print(cl);
  def sett (val):
    cl = val
  return gett, sett

ed eseguendo:

g, s = make()
g() #1
s(2); g() #1
s(3); g() #1

Motivo: come molti altri hanno detto sopra, in Python, se esiste un'assegnazione nell'ambito interno a una variabile con lo stesso nome, viene creato un nuovo riferimento nell'ambito interno. Non così con JS, a meno che tu non lo dichiari esplicitamente con la varparola chiave.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.