2013-11-04 36 views
6

Mi sono imbattuto in un comportamento in python che ho difficoltà a capire. Questo è il codice proof-of-concept:Comportamento lambda strano nei loop

from functools import partial 

if __name__ == '__main__': 
    sequence = ['foo', 'bar', 'spam'] 
    loop_one = lambda seq: [lambda: el for el in seq] 
    no_op = lambda x: x 
    loop_two = lambda seq: [partial(no_op, el) for el in seq] 
    for func in (loop_one, loop_two): 
     print [f() for f in func(sequence)] 

L'uscita di quanto sopra è:

['spam', 'spam', 'spam'] 
['foo', 'bar', 'spam'] 

Il comportamento di loop_one è sorprendente per me come mi sarei aspettato che a comportarsi come loop_two: el è un valore immutabile (una stringa) che cambia ad ogni ciclo, ma lambda sembra memorizzare un puntatore alla "variabile di loop", come se il ciclo ricicli lo stesso indirizzo di memoria per ciascun elemento della sequenza.

Il comportamento sopra riportato è lo stesso con le funzioni complete con un ciclo for in esse (quindi non è una sintassi di tipo list-comprehension).

Ma aspetta: c'è di più ... e più sconcertante!

Il seguente script funziona come loop_one:

b = [] 
for foo in ("foo", "bar"): 
    b.append(lambda: foo) 

print [a() for a in b] 

(uscita: ['bar', 'bar'])

Ma guarda cosa succede quando uno sostituiamo il nome della variabile foo con a:

b = [] 
for a in ("foo", "bar"): 
    b.append(lambda: a) 

print [a() for a in b] 

(uscita: [<function <lambda> at 0x25cce60>, <function <lambda> at 0x25cced8>])

Qualche idea di cosa sta succedendo qui? Sospetto che ci debba essere qualche trucco legato all'implementazione C sottostante del mio interprete, ma non ho nient'altro (Jthon, PyPy o simili) per verificare se questo comportamento è coerente tra diverse implementazioni.

risposta

4

La funzione lambda: el utilizzato in loop_one riferisce a una variabile el non definita nell'ambito locale. Pertanto, Python cerca accanto nell'ambito racchiude dell'altro lambda:

lambda seq: [lambda: el for el in seq] 

secondo la cosiddetta LEGB rule.

Nel momento in cui viene chiamato lambda: el, questo lambda di inclusione (ovviamente) è già stato chiamato e la comprensione della lista è stata valutata. Lo el utilizzato nella comprensione dell'elenco è una variabile locale in questo lambda di chiusura. Il suo valore è quello restituito quando Python cerca il valore di el in lambda: el. Questo valore per el è uguale a per tutte le diverse funzioni lambda: el nella comprensione dell'elenco: è l'ultimo valore assegnato a el nel ciclo for el in seq.Pertanto, el è sempre 'spam', l'ultimo valore in seq.


hai già trovato una soluzione, per usare una chiusura come il tuo loop_two. Un altro modo è definire el come una variabile locale con un valore predefinito:

loop_one = lambda seq: [lambda el=el: el for el in seq] 
3

Le variabili (foo nell'esempio seguente) sono vincolate non quando viene creata la lambda, ma quando viene richiamata la lambda.

>>> b = [] 
>>> for foo in ("foo", "bar"): 
...  b.append(lambda: foo) 
... 
>>> foo = "spam" 
>>> print [a() for a in b] 
['spam', 'spam'] 

>>> b = [] 
>>> for foo in ("foo", "bar"): 
...  b.append(lambda foo=foo: foo) 
... 
>>> print [a() for a in b] 
['foo', 'bar']