Escopo das funções lambda e seus parâmetros?

92

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.

Agartland
fonte
3
O título da sua pergunta é um pouco enganador.
lispmachine
1
Por que usar lambdas se você os acha confusos? Por que não usar def para definir funções? O que há no seu problema que torna os lambdas tão importantes?
S.Lott
A função aninhada @ S.Lott resultará no mesmo problema (talvez mais claramente visível)
lispmachine
1
@agartland: Você sou eu? Eu também estava trabalhando em eventos de GUI e escrevi o seguinte teste quase idêntico antes de encontrar esta página durante a pesquisa de fundo: pastebin.com/M5jjHjFT
imallett
5
Consulte Por que lambdas definidos em um loop com valores diferentes retornam o mesmo resultado? no FAQ oficial de programação para Python. Ele explica o problema muito bem e oferece uma solução.
abarnert

Respostas:

80

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 é o callback_factoryescopo que é criado uma vez a cada callback_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()
lispmachine
fonte
2
Esta explicação é um pouco enganosa. O problema é a mudança de valor de m na iteração, não o escopo.
Ixx
O comentário acima é verdadeiro, conforme observado por @abarnert no comentário sobre a questão em que também é fornecido um link que explica o phenonimon e a solução. O método de fábrica oferece o mesmo efeito que o argumento para o método de fábrica tem o efeito de criar uma nova variável com escopo local para o lambda. No entanto, a solição fornecida não funciona sintaticamente, pois não há argumentos para o lambda - e a solução lambda in lamda abaixo também oferece o mesmo efeito sem criar um novo método persistente para criar um lambda
Mark Parris
134

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ável mtem valor 'mi'. Portanto, quando você realmente executar a função que criou posteriormente, ela pesquisará o valor de mno ambiente que a criou, que nessa altura terá valor 'mi'.

Uma solução comum e idiomática para esse problema é capturar o valor de mno 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))
newacct
fonte
6
Ótima solução! Embora complicado, acho que o significado original é mais claro do que com outras sintaxes.
Quantum7
3
Não há nada hackeado ou complicado nisso; é exatamente a mesma solução que o FAQ oficial do Python sugere. Veja aqui .
abarnert
3
@abernert, "hackish and tricky" não é necessariamente incompatível com "ser a solução que o FAQ oficial do Python sugere". Obrigado pela referência.
Don Hatch
1
reutilizar o mesmo nome de variável não é claro para quem não está familiarizado com esse conceito. A ilustração seria melhor se fosse lambda n = m. Sim, você teria que alterar seu parâmetro de retorno de chamada, mas o corpo do loop for pode permanecer o mesmo, eu acho.
Nick
1
+1 Totalmente! Esta deve ser a solução ... A solução aceita não é tão boa quanto esta. Afinal, estamos tentando usar funções lambda aqui ... Não simplesmente mover tudo para uma defdeclaração.
255.tar.xz
6

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 :)

Nicolas Dumazet
fonte
1

Não está diretamente relacionado ao problema em questão, mas é uma sabedoria inestimável: Python Objects de Fredrik Lundh.

tzot
fonte
1
Não diretamente relacionado à sua resposta, mas uma pesquisa por gatinhos: google.com/search?q=kitten
Singletoned em
@Singletoned: se o OP grocasse o artigo para o qual forneci um link, ele não faria a pergunta em primeiro lugar; é por isso que está indiretamente relacionado. Tenho certeza de que você ficará encantado em me explicar como os gatinhos estão indiretamente relacionados à minha resposta (através de uma abordagem holística, eu presumo;)
tzot
1

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))
Benoît
fonte
1

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 de ia j no

cada vez que o exterior lambdaé chamado ele faz uma instância do interior lambdacom jligado ao valor actual de icomo ivalor de

Aaron Goldman
fonte
0

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:

O problema de terminologia pode ser devido ao fato de que, em python, o valor de um nome é uma referência a um objeto. Então, você sempre passa o valor (sem cópia implícita), e esse valor é sempre uma referência. [...] Agora, se você quiser cunhar um nome para isso, como "por referência de objeto", "por valor não copiado", ou qualquer outra coisa, fique à vontade. Tentar reutilizar a terminologia que é mais geralmente aplicada a linguagens onde "variáveis ​​são caixas" para uma linguagem onde "variáveis ​​são tags post-it" é, IMHO, mais provável de confundir do que ajudar.

Yuval Adam
fonte
0

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:

do
re
mi
Jon Skeet
fonte
0

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 :)

Tom
fonte
Quando você diz "no sentido clássico", quer dizer "como C fez". Muitas linguagens, incluindo Python, implementam variáveis ​​de maneira diferente de C.
Ned Batchelder
0

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 iage como a fábrica nas demais respostas.

YvesgereY
fonte