Cosa acquisiscono le chiusure (lambda)?


249

Di recente ho iniziato a giocare con Python e ho scoperto qualcosa di strano nel modo in cui funzionano le chiusure. Considera il seguente codice:

adders=[0,1,2,3]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

Costruisce un semplice array di funzioni che accettano un singolo input e restituiscono quell'input aggiunto da un numero. Le funzioni sono costruiti in forciclo in cui l'iteratore iva da 0a 3. Per ciascuno di questi numeri lambdaviene creata una funzione che acquisisce ie la aggiunge all'input della funzione. L'ultima riga chiama la seconda lambdafunzione con3 come parametro. Con mia sorpresa l'output è stato 6.

Mi aspettavo a 4. Il mio ragionamento era: in Python ogni cosa è un oggetto e quindi ogni variabile è essenziale un puntatore ad esso. Durante la creazione delle lambdachiusure per i, mi aspettavo che memorizzasse un puntatore all'oggetto intero attualmente indicato da i. Ciò significa che quando iassegnato un nuovo oggetto intero non dovrebbe influenzare le chiusure precedentemente create. Purtroppo, l'ispezione addersdell'array all'interno di un debugger mostra che lo fa. Tutte le lambdafunzioni si riferiscono all'ultimo valore i, 3, che si traduce in adders[1](3)ritorno 6.

Il che mi fa chiedere quanto segue:

  • Cosa catturano esattamente le chiusure?
  • Qual è il modo più elegante per convincere le lambdafunzioni a catturare il valore attuale iin un modo che non sarà influenzato quando icambia il suo valore?

35
Ho avuto questo problema nel codice dell'interfaccia utente. Mi ha fatto impazzire. Il trucco è ricordare che i loop non creano un nuovo ambito.
Detly

3
@TimMB Come si ilascia lo spazio dei nomi?
detly

3
@detly Beh, stavo per dire che print inon avrebbe funzionato dopo il ciclo. Ma l'ho testato da solo e ora capisco cosa intendi: funziona. Non avevo idea che le variabili del loop indugiassero dopo il corpo del loop in Python.
Tim MB,

1
@TimMB - Sì, questo è ciò che intendevo. Lo stesso vale per if, with, tryecc
detly

13
Questo è nelle FAQ ufficiali di Python, in Perché i lambda definiti in un ciclo con valori diversi restituiscono tutti lo stesso risultato? , con una spiegazione e la solita soluzione alternativa.
Abarnert,

Risposte:


161

La tua seconda domanda ha avuto risposta, ma per quanto riguarda la tua prima:

cosa cattura esattamente la chiusura?

Lo scoping in Python è dinamico e lessicale. Una chiusura ricorderà sempre il nome e l'ambito della variabile, non l'oggetto a cui punta. Poiché tutte le funzioni del tuo esempio sono create nello stesso ambito e usano lo stesso nome di variabile, si riferiscono sempre alla stessa variabile.

EDIT: Per quanto riguarda l'altra tua domanda su come superare questo, ci sono due modi che vengono in mente:

  1. Il modo più conciso, ma non strettamente equivalente, è quello raccomandato da Adrien Plisson . Crea un lambda con un argomento aggiuntivo e imposta il valore predefinito dell'argomento aggiuntivo sull'oggetto che desideri conservare.

  2. Un po 'più prolisso ma meno confuso sarebbe creare un nuovo ambito ogni volta che crei la lambda:

    >>> adders = [0,1,2,3]
    >>> for i in [0,1,2,3]:
    ...     adders[i] = (lambda b: lambda a: b + a)(i)
    ...     
    >>> adders[1](3)
    4
    >>> adders[2](3)
    5

    L'ambito qui viene creato utilizzando una nuova funzione (una lambda, per brevità), che lega il suo argomento e passa il valore che si desidera associare come argomento. Nel codice reale, tuttavia, molto probabilmente avrai una funzione ordinaria anziché lambda per creare il nuovo ambito:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]

1
Max, se aggiungi una risposta per l'altra mia (domanda più semplice), posso contrassegnarla come una risposta accettata. Grazie!
Boaz,

3
Python ha un ambito statico, non un ambito dinamico ... è solo che tutte le variabili sono riferimenti, quindi quando si imposta una variabile su un nuovo oggetto, la variabile stessa (il riferimento) ha la stessa posizione, ma punta a qualcos'altro. la stessa cosa accade in Scheme se tu set!. vedi qui per cosa sia realmente l'ambito dinamico: voidspace.org.uk/python/articles/code_blocks.shtml .
Claudiu,

6
L'opzione 2 ricorda ciò che i linguaggi funzionali chiamerebbero una "funzione al curry".
Crashworks,

205

puoi forzare l'acquisizione di una variabile usando un argomento con un valore predefinito:

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

l'idea è di dichiarare un parametro (abilmente chiamato i) e dargli un valore predefinito della variabile che si desidera acquisire (il valore di i)


7
+1 per l'utilizzo dei valori predefiniti. Essere valutati quando la lambda è definita li rende perfetti per questo uso.
Quornian,

21
+1 anche perché questa è la soluzione approvata dalle FAQ ufficiali .
Abarnert,

23
Questo è fantastico Il comportamento predefinito di Python, tuttavia, non lo è.
Cecil Curry,

1
Tuttavia, questa non sembra una buona soluzione ... in realtà stai cambiando la firma della funzione solo per acquisire una copia della variabile. E anche quelli che invocano la funzione possono pasticciare con la variabile i, giusto?
David Callanan,

@DavidCallanan stiamo parlando di un lambda: un tipo di funzione ad-hoc che in genere definisci nel tuo codice per tappare un buco, non qualcosa che condividi attraverso un intero sdk. se hai bisogno di una firma più forte, dovresti usare una funzione reale.
Adrien Plisson,

33

Per completezza, un'altra risposta alla tua seconda domanda: è possibile utilizzare parziale nei functools modulo .

Con l'importazione di aggiungere dall'operatore come proposto da Chris Lutz, l'esempio diventa:

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
   # store callable object with first argument given as (current) i
   adders[i] = partial(add, i) 

print adders[1](3)

24

Considera il seguente codice:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

Penso che molte persone non lo troveranno affatto confuso. È il comportamento previsto.

Quindi, perché la gente pensa che sarebbe diverso se fatto in un ciclo? So di aver fatto quell'errore da solo, ma non so perché. È il ciclo? O forse la lambda?

Dopotutto, il loop è solo una versione più breve di:

adders= [0,1,2,3]
i = 0
adders[i] = lambda a: i+a
i = 1
adders[i] = lambda a: i+a
i = 2
adders[i] = lambda a: i+a
i = 3
adders[i] = lambda a: i+a

11
È il ciclo, perché in molte altre lingue un ciclo può creare un nuovo ambito.
Detly

1
Questa risposta è buona perché spiega perché isi accede alla stessa variabile per ciascuna funzione lambda.
David Callanan,

3

In risposta alla tua seconda domanda, il modo più elegante per farlo sarebbe usare una funzione che accetta due parametri invece di un array:

add = lambda a, b: a + b
add(1, 3)

Tuttavia, usare lambda qui è un po 'sciocco. Python ci fornisce il operatormodulo, che fornisce un'interfaccia funzionale agli operatori di base. La lambda sopra ha un sovraccarico non necessario solo per chiamare l'operatore addizione:

from operator import add
add(1, 3)

Capisco che stai giocando, cercando di esplorare la lingua, ma non riesco a immaginare una situazione in cui utilizzerei una serie di funzioni in cui la stranezza di scoping di Python si frappone.

Se lo si desidera, è possibile scrivere una piccola classe che utilizza la sintassi di indicizzazione dell'array:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)

2
Chris, ovviamente il codice sopra non ha nulla a che fare con il mio problema originale. È costruito per illustrare il mio punto in modo semplice. Ovviamente è inutile e sciocco.
Boaz,

3

Ecco un nuovo esempio che evidenzia la struttura dei dati e il contenuto di una chiusura, per aiutare a chiarire quando il contesto che lo circonda viene "salvato".

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

Cosa c'è in una chiusura?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

In particolare, my_str non è nella chiusura di f1.

Cosa c'è nella chiusura di f2?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

Si noti (dagli indirizzi di memoria) che entrambe le chiusure contengono gli stessi oggetti. Quindi puoi iniziare a pensare alla funzione lambda come avere un riferimento all'ambito. Tuttavia, my_str non è nella chiusura per f_1 o f_2 e i non è nella chiusura per f_3 (non mostrato), il che suggerisce che gli oggetti di chiusura stessi sono oggetti distinti.

Gli stessi oggetti di chiusura sono lo stesso oggetto?

>>> print f_1.func_closure is f_2.func_closure
False

NB L'output int object at [address X]>mi ha fatto pensare che la chiusura stia memorizzando [indirizzo X] AKA come riferimento. Tuttavia, [indirizzo X] cambierà se la variabile viene riassegnata dopo l'istruzione lambda.
Jeff,
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.