Preciso de uma função de retorno de chamada que seja quase exatamente a mesma para uma série de eventos gui. A função terá um comportamento ligeiramente diferente, dependendo de qual evento a chamou. Parece um caso simples para mim, mas não consigo entender esse comportamento estranho das funções lambda.
Portanto, tenho o seguinte código simplificado abaixo:
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()
A saída deste código é:
mi
mi
mi
do
re
mi
Eu esperava:
do
re
mi
do
re
mi
Por que usar um iterador bagunçou as coisas?
Eu tentei usar uma deepcopy:
import copy
funcList=[]
for m in ('do', 're', 'mi'):
funcList.append(lambda: callback(copy.deepcopy(m)))
for f in funcList:
f()
Mas isso tem o mesmo problema.
python
lexical-closures
Agartland
fonte
fonte
Respostas:
O problema aqui é o
m
variável (uma referência) sendo tirada do escopo circundante. Apenas os parâmetros são mantidos no escopo lambda.Para resolver isso, você deve criar outro escopo para 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()
No exemplo acima, lambda também usa o escopo circundante para localizar
m
, mas desta vez é ocallback_factory
escopo que é criado uma vez a cadacallback_factory
chamada.Ou com 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()
fonte
Quando um lambda é criado, ele não faz uma cópia das variáveis no escopo delimitador que usa. Ele mantém uma referência ao ambiente para que possa consultar o valor da variável posteriormente. Só existe um
m
. Ele é atribuído a cada vez por meio do loop. Após o loop, a variávelm
tem valor'mi'
. Portanto, quando você realmente executar a função que criou posteriormente, ela pesquisará o valor dem
no ambiente que a criou, que nessa altura terá valor'mi'
.Uma solução comum e idiomática para esse problema é capturar o valor de
m
no momento em que o lambda é criado, usando-o como o argumento padrão de um parâmetro opcional. Normalmente, você usa um parâmetro com o mesmo nome para não ter que alterar o corpo do código:for m in ('do', 're', 'mi'): funcList.append(lambda m=m: callback(m))
fonte
def
declaração.Python usa referências, é claro, mas isso não importa neste contexto.
Quando você define um lambda (ou uma função, já que é exatamente o mesmo comportamento), ele não avalia a expressão lambda antes do tempo de execução:
# defining that function is perfectly fine def broken(): print undefined_var broken() # but calling it will raise a NameError
Ainda mais surpreendente do que seu exemplo lambda:
i = 'bar' def foo(): print i foo() # bar i = 'banana' foo() # you would expect 'bar' here? well it prints 'banana'
Resumindo, pense dinâmico: nada é avaliado antes da interpretação, é por isso que seu código usa o valor mais recente de m.
Quando ele procura m na execução de lambda, m é retirado do escopo mais alto, o que significa que, como outros apontaram; você pode contornar esse problema adicionando outro escopo:
def factory(x): return lambda: callback(x) for m in ('do', 're', 'mi'): funcList.append(factory(m))
Aqui, quando o lambda é chamado, ele procura um x no escopo da definição do lambda. Este x é uma variável local definida no corpo da fábrica. Por isso, o valor usado na execução do lambda será o valor que foi passado como parâmetro durante a chamada à fábrica. E doremi!
Como nota, eu poderia ter definido fábrica como fábrica (m) [substitua x por m], o comportamento é o mesmo. Usei um nome diferente para maior clareza :)
Você pode descobrir que Andrej Bauer tem problemas de lambda semelhantes. O que é interessante nesse blog são os comentários, onde você aprenderá mais sobre o fechamento do python :)
fonte
Não está diretamente relacionado ao problema em questão, mas é uma sabedoria inestimável: Python Objects de Fredrik Lundh.
fonte
Sim, isso é um problema de escopo, ele se liga ao m externo, esteja você usando um lambda ou uma função local. Em vez disso, use um functor:
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))
fonte
o soluiton para lambda é mais 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']
o externo
lambda
é usado para ligar o valor atual dei
aj
nocada vez que o exterior
lambda
é chamado ele faz uma instância do interiorlambda
comj
ligado ao valor actual dei
comoi
valor defonte
Em primeiro lugar, o que você está vendo não é um problema e não está relacionado a uma chamada por referência ou por valor.
A sintaxe lambda que você definiu não tem parâmetros e, como tal, o escopo que você está vendo com o parâmetro
m
é externo à função lambda. É por isso que você está vendo esses resultados.A sintaxe Lambda, em seu exemplo, não é necessária, e você prefere usar uma chamada de função simples:
for m in ('do', 're', 'mi'): callback(m)
Novamente, você deve ser muito preciso sobre quais parâmetros lambda está usando e onde exatamente seu escopo começa e termina.
Como uma observação lateral, em relação à passagem de parâmetros. Os parâmetros em python são sempre referências a objetos. Para citar Alex Martelli:
fonte
A variável
m
está sendo capturada, portanto, sua expressão lambda sempre vê seu valor "atual".Se você precisar capturar efetivamente o valor em um momento no tempo, escreva uma função que recebe o valor que você deseja como parâmetro e retorna uma expressão lambda. Nesse ponto, o lambda irá capturar o valor do parâmetro , que não mudará quando você chamar a função várias vezes:
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()
Resultado:
fonte
na verdade, não há variáveis no sentido clássico em Python, apenas nomes que foram vinculados por referências ao objeto aplicável. Mesmo as funções são algum tipo de objeto em Python, e lambdas não abrem exceção à regra :)
fonte
Como uma nota lateral
map
, embora desprezado por alguma figura Python bem conhecida, obriga a uma construção que evita essa armadilha.fs = map (lambda i: lambda: callback (i), ['do', 're', 'mi'])
NB: o primeiro
lambda i
age como a fábrica nas demais respostas.fonte