Ambito delle funzioni lambda e dei loro parametri?


92

Ho bisogno di una funzione di callback che sia quasi esattamente la stessa per una serie di eventi gui. La funzione si comporterà in modo leggermente diverso a seconda dell'evento che l'ha chiamata. Mi sembra un caso semplice, ma non riesco a capire questo strano comportamento delle funzioni lambda.

Quindi ho il seguente codice semplificato di seguito:

def callback(msg):
    print msg

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(m))
for f in funcList:
    f()

#create one at a time
funcList=[]
funcList.append(lambda: callback('do'))
funcList.append(lambda: callback('re'))
funcList.append(lambda: callback('mi'))
for f in funcList:
    f()

L'output di questo codice è:

mi
mi
mi
do
re
mi

Mi aspettavo:

do
re
mi
do
re
mi

Perché l'uso di un iteratore ha incasinato le cose?

Ho provato a utilizzare una copia profonda:

import copy
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(copy.deepcopy(m)))
for f in funcList:
    f()

Ma questo ha lo stesso problema.


3
Il titolo della tua domanda è in qualche modo fuorviante.
lispmachine

1
Perché usare lambda se li trovi confusi? Perché non usare def per definire le funzioni? Qual è il tuo problema che rende gli lambda così importanti?
S.Lott

@ S.Lott La funzione annidata risulterà nello stesso problema (forse più chiaramente visibile)
lispmachine,

1
@agartland: sei io? Anch'io stavo lavorando su eventi GUI e ho scritto il seguente test quasi identico prima di trovare questa pagina durante la ricerca di background: pastebin.com/M5jjHjFT
imallett

5
Vedere Perché i lambda definiti in un ciclo con valori diversi restituiscono tutti lo stesso risultato? nelle FAQ ufficiali sulla programmazione per Python. Spiega abbastanza bene il problema e offre una soluzione.
abarnert

Risposte:


80

Il problema qui è la mvariabile (un riferimento) che viene presa dall'ambito circostante. Nell'ambito lambda vengono mantenuti solo i parametri.

Per risolvere questo problema devi creare un altro ambito per lambda:

def callback(msg):
    print msg

def callback_factory(m):
    return lambda: callback(m)

funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(callback_factory(m))
for f in funcList:
    f()

Nell'esempio sopra, lambda usa anche l'ambito circostante per trovare m, ma questa volta è l' callback_factoryambito che viene creato una volta per ogni callback_factory chiamata.

O con functools.partial :

from functools import partial

def callback(msg):
    print msg

funcList=[partial(callback, m) for m in ('do', 're', 'mi')]
for f in funcList:
    f()

2
Questa spiegazione è un po 'fuorviante. Il problema è il cambio di valore di m nell'iterazione, non l'ambito.
Ixx

Il commento sopra è vero come notato da @abarnert nel commento alla domanda in cui viene fornito anche un collegamento che spiega il phenonimon e la soluzione. Il metodo factory fornisce lo stesso effetto dell'argomento del metodo factory ha l'effetto di creare una nuova variabile con ambito locale per lambda. Tuttavia la solizione data non funziona sintatticamente in quanto non ci sono argomenti per il lambda - e anche la soluzione lambda nella soluzione lamda di seguito offre lo stesso effetto senza creare un nuovo metodo persistente per creare un lambda
Mark Parris

134

Quando viene creato un lambda, non crea una copia delle variabili nell'ambito di inclusione che utilizza. Mantiene un riferimento all'ambiente in modo che possa cercare il valore della variabile in un secondo momento. Ce n'è solo uno m. Viene assegnato ogni volta attraverso il ciclo. Dopo il ciclo, la variabile mha valore 'mi'. Quindi, quando esegui effettivamente la funzione che hai creato in seguito, cercherà il valore mnell'ambiente che l'ha creata, che a quel punto avrà valore 'mi'.

Una soluzione comune e idiomatica a questo problema è acquisire il valore di mnel momento in cui viene creato il lambda utilizzandolo come argomento predefinito di un parametro opzionale. Di solito usi un parametro con lo stesso nome, quindi non devi cambiare il corpo del codice:

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))

6
Bella soluzione! Sebbene complicato, ritengo che il significato originale sia più chiaro rispetto ad altre sintassi.
Quantum7

3
Non c'è niente di hackish o complicato in questo; è esattamente la stessa soluzione suggerita dalle FAQ ufficiali di Python. Vedi qui .
abarnert

3
@abernert, "hackish and tricky" non è necessariamente incompatibile con "essere la soluzione suggerita dalle FAQ ufficiali di Python". Grazie per il riferimento.
Don Hatch il

1
riutilizzare lo stesso nome di variabile non è chiaro a chi non ha familiarità con questo concetto. L'illustrazione sarebbe migliore se fosse lambda n = m. Sì, dovresti cambiare il tuo parametro di richiamata, ma il corpo del ciclo for potrebbe rimanere lo stesso, penso.
Nick

1
+1 fino in fondo! Questa dovrebbe essere la soluzione ... La soluzione accettata non è così buona come questa. Dopo tutto, stiamo cercando di usare le funzioni lambda qui ... Non semplicemente spostare tutto in una defdichiarazione.
255.tar.xz

6

Python usa i riferimenti ovviamente, ma non importa in questo contesto.

Quando definisci un lambda (o una funzione, poiché questo è esattamente lo stesso comportamento), non valuta l'espressione lambda prima del runtime:

# defining that function is perfectly fine
def broken():
    print undefined_var

broken() # but calling it will raise a NameError

Ancora più sorprendente del tuo esempio lambda:

i = 'bar'
def foo():
    print i

foo() # bar

i = 'banana'

foo() # you would expect 'bar' here? well it prints 'banana'

In breve, pensa dinamico: nulla viene valutato prima dell'interpretazione, ecco perché il tuo codice utilizza l'ultimo valore di m.

Quando cerca m nell'esecuzione lambda, m è preso dallo scope più in alto, il che significa che, come altri hanno sottolineato; puoi aggirare il problema aggiungendo un altro ambito:

def factory(x):
    return lambda: callback(x)

for m in ('do', 're', 'mi'):
    funcList.append(factory(m))

Qui, quando viene chiamato lambda, cerca una x nell'ambito della definizione lambda. Questa x è una variabile locale definita nel corpo della fabbrica. Per questo motivo, il valore utilizzato nell'esecuzione lambda sarà il valore passato come parametro durante la chiamata a factory. E doremi!

Come nota, avrei potuto definire factory come factory (m) [sostituisci x con m], il comportamento è lo stesso. Ho usato un nome diverso per chiarezza :)

Potresti scoprire che Andrej Bauer ha problemi lambda simili. Ciò che è interessante in quel blog sono i commenti, dove imparerai di più sulla chiusura di Python :)


1

Non direttamente correlato al problema in questione, ma comunque un prezioso pezzo di saggezza: Python Objects di Fredrik Lundh.


1
Non direttamente correlato alla tua risposta, ma una ricerca di gattini: google.com/search?q=kitten
Singletoned

@ Singletoned: se l'OP scrutasse l'articolo a cui ho fornito un collegamento, non farebbero la domanda in primo luogo; ecco perché è indirettamente correlato. Sono sicuro che sarai felice di spiegarmi come i gattini siano indirettamente correlati alla mia risposta (attraverso un approccio olistico, presumo;)
tzot

1

Sì, questo è un problema di ambito, si lega alla m esterna, sia che tu stia usando un lambda o una funzione locale. Invece, usa un funtore:

class Func1(object):
    def __init__(self, callback, message):
        self.callback = callback
        self.message = message
    def __call__(self):
        return self.callback(self.message)
funcList.append(Func1(callback, m))

1

la soluzione per lambda è più lambda

In [0]: funcs = [(lambda j: (lambda: j))(i) for i in ('do', 're', 'mi')]

In [1]: funcs
Out[1]: 
[<function __main__.<lambda>>,
 <function __main__.<lambda>>,
 <function __main__.<lambda>>]

In [2]: [f() for f in funcs]
Out[2]: ['do', 're', 'mi']

l'esterno lambdaviene utilizzato per associare il valore corrente di ia j in

ogni volta l'esterno lambdaè denominato rende un'istanza di quella interna lambdacon jlegata al valore corrente icome ivalore 's


0

In primo luogo, ciò che stai vedendo non è un problema e non è correlato alla chiamata per riferimento o al valore.

La sintassi lambda che hai definito non ha parametri e, in quanto tale, l'ambito visualizzato con il parametro mè esterno alla funzione lambda. Questo è il motivo per cui stai vedendo questi risultati.

La sintassi Lambda, nel tuo esempio non è necessaria e preferiresti utilizzare una semplice chiamata di funzione:

for m in ('do', 're', 'mi'):
    callback(m)

Ancora una volta, dovresti essere molto preciso su quali parametri lambda stai utilizzando e dove esattamente inizia e finisce il loro ambito.

Come nota a margine, per quanto riguarda il passaggio di parametri. I parametri in Python sono sempre riferimenti a oggetti. Per citare Alex Martelli:

Il problema terminologico potrebbe essere dovuto al fatto che, in python, il valore di un nome è un riferimento a un oggetto. Quindi, si passa sempre il valore (nessuna copia implicita) e quel valore è sempre un riferimento. [...] Ora, se vuoi coniare un nome per quello, come "per riferimento a un oggetto", "per valore non copiato", o qualsiasi altra cosa, sii mio ospite. Tentare di riutilizzare la terminologia che è più generalmente applicata ai linguaggi in cui "le variabili sono scatole" a un linguaggio in cui "le variabili sono tag post-it" è, IMHO, più probabile che confonda che che aiuti.


0

La variabile mviene acquisita, quindi la tua espressione lambda vede sempre il suo valore "corrente".

Se è necessario acquisire in modo efficace il valore in un momento nel tempo, scrivere una funzione prende il valore desiderato come parametro e restituisce un'espressione lambda. A quel punto, lambda acquisirà il valore del parametro , che non cambierà quando chiami la funzione più volte:

def callback(msg):
    print msg

def createCallback(msg):
    return lambda: callback(msg)

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(createCallback(m))
for f in funcList:
    f()

Produzione:

do
re
mi

0

in realtà non ci sono variabili in senso classico in Python, solo nomi che sono stati vincolati da riferimenti all'oggetto applicabile. Anche le funzioni sono una sorta di oggetto in Python e le lambda non fanno eccezione alla regola :)


Quando dici "nel senso classico", intendi "come ha fatto il C". Molti linguaggi, incluso Python, implementano le variabili in modo diverso da C.
Ned Batchelder

0

Come nota a mapmargine, sebbene disprezzato da qualche noto personaggio di Python, costringe una costruzione che prevenga questa trappola.

fs = map (lambda i: lambda: callback (i), ['do', 're', 'mi'])

NB: il primo si lambda icomporta come la fabbrica in altre risposte.

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.