lambda函数的范围及其参数?

aga*_*and 79 python lexical-closures

我需要一个回调函数,对于一系列gui事件几乎完全相同.该函数的行为会略有不同,具体取决于调用它的事件.对我来说似乎是一个简单的案例,但我无法弄清楚lambda函数的这种奇怪的行为.

所以我在下面有以下简化代码:

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()
Run Code Online (Sandbox Code Playgroud)

此代码的输出是:

mi
mi
mi
do
re
mi
Run Code Online (Sandbox Code Playgroud)

我期望:

do
re
mi
do
re
mi
Run Code Online (Sandbox Code Playgroud)

为什么使用迭代器搞砸了?

我尝试过使用深度镜:

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

但这也存在同样的问题.

new*_*cct 121

创建lambda时,它不会复制它使用的封闭范围中的变量.它维护对环境的引用,以便以后可以查找变量的值.只有一个m.它通过循环每次都被分配.循环之后,变量m具有值'mi'.因此,当您实际运行稍后创建的函数时,它将查找m创建它的环境中的值,然后该值将具有值'mi'.

这个问题的一个常见和惯用的解决方案m是通过使用它作为可选参数的默认参数来捕获创建lambda时的值.您通常使用相同名称的参数,因此您不必更改代码的主体:

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))
Run Code Online (Sandbox Code Playgroud)

  • 好的解决方案 虽然很棘手,但我觉得原来的意思比其他语法更清晰. (6认同)
  • ha and和令人敬畏 (4认同)
  • 这一点一点也不乏味或棘手。这与官方Python FAQ建议的解决方案完全相同。参见[here](https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same -结果)。 (2认同)
  • @abernert,“笨拙而棘手的”不一定与“成为官方Python FAQ建议的解决方案”不兼容。感谢您的参考。 (2认同)

lis*_*ine 71

这里的问题是m从周围范围中取得的变量(参考).只有参数保存在lambda范围内.

要解决这个问题,你必须为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()
Run Code Online (Sandbox Code Playgroud)

在上面的例子中,lambda也使用周围的范围来查找m,但这次callback_factory是每个callback_factory 调用创建一次的范围.

或者使用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()
Run Code Online (Sandbox Code Playgroud)

  • 这个解释有点误导。问题是迭代中m值的变化,而不是范围。 (2认同)

Nic*_*zet 6

Python当然使用引用,但在这种情况下无关紧要.

当您定义lambda(或函数,因为这是完全相同的行为)时,它不会在运行时之前计算lambda表达式:

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

broken() # but calling it will raise a NameError
Run Code Online (Sandbox Code Playgroud)

比你的lambda例子更令人惊讶:

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

foo() # bar

i = 'banana'

foo() # you would expect 'bar' here? well it prints 'banana'
Run Code Online (Sandbox Code Playgroud)

简而言之,思考动态:在解释之前没有任何评估,这就是为什么你的代码使用最新的m值.

当它在lambda执行中查找m时,m取自最顶层的范围,这意味着,正如其他人指出的那样; 你可以通过添加另一个范围来规避这个问题:

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

for m in ('do', 're', 'mi'):
    funcList.append(factory(m))
Run Code Online (Sandbox Code Playgroud)

这里,当调用lambda时,它会在lambda的定义范围中查找x.这个x是工厂正文中定义的局部变量.因此,lambda执行时使用的值将是在调用factory期间作为参数传递的值.而doremi!

作为一个注释,我可以将工厂定义为工厂(m)[用x替换x],行为是相同的.为清晰起见,我使用了不同的名称:)

你可能会发现Andrej Bauer有类似的lambda问题.这个博客上有趣的是评论,你将在那里了解更多关于python封闭的信息:)