Lambda函数的范围及其参数?


89

我需要一个与一系列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()

此代码的输出是:

mi
mi
mi
do
re
mi

我期望:

do
re
mi
do
re
mi

为什么使用迭代器弄乱了事情?

我试过使用Deepcopy:

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

但这有同样的问题。


3
您的问题标题有些误导。
lispmachine

1
如果发现lambda令人困惑,为什么还要使用它们呢?为什么不使用def定义函数?令lambda如此重要的问题是什么?
S.Lott

@ S.Lott嵌套函数将导致相同的问题(也许更清楚可见)
lispmachine

1
@agartland:你是我吗?我也正在处理GUI事件,因此在进行背景研究期间找到此页面之前,我写了以下几乎相同的测试:pastebin.com/M5jjHjFT
malletet

5
请参见为什么在循环中定义的具有不同值的lambda全部返回相同的结果?在Python的官方编程常见问题解答中。它很好地解释了问题,并提供了解决方案。
abarnert,2014年

Answers:


79

这里的问题是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()

在上面的示例中,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()

2
这种解释有点误导。问题在于迭代中m的值变化,而不是范围。
Ixx 2012年

@abarnert在对该问题的评论中指出,上面的评论为true,其中还给出了一个解释现象和解决方案的链接。工厂方法的效果与工厂方法的参数相同,效果是创建一个范围为lambda局部的新变量。但是,由于没有针对lambda的论点,因此给定的隔离语法无法协同工作-并且下面的lamda解决方案中的lambda也可产生相同的效果,而无需创建新的持久方法来创建lambda
Mark Parris

132

创建lambda时,它不会在其使用的范围内复制变量。它维护对环境的引用,以便以后可以查找变量的值。只有一个m。它在循环中每次都分配给它。循环后,变量m具有value 'mi'。因此,当您实际运行稍后创建的函数时,它将在创建该函数m的环境中查找的值,届时将具有value 'mi'

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

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

我的意思是带有默认值的可选参数
lispmachine

6
不错的解决方案!尽管比较棘手,但我觉得原始含义比其他语法更清晰。
Quantum7

3
这一点一点也不乏味或棘手。这与官方Python FAQ建议的解决方案完全相同。看这里
abarnert,2014年

3
@abernert,“笨拙而棘手”不一定与“成为官方Python FAQ建议的解决方案”不兼容。感谢您的参考。
唐·哈奇

1
不熟悉此概念的人不清楚重用相同的变量名。如果为lambda n = m,说明会更好。是的,您必须更改您的回调参数,但是for循环主体可以保持不变。
尼克

6

当然,Python确实使用了引用,但是在这种情况下并不重要。

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

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

broken() # but calling it will raise a NameError

比您的lambda示例更令人惊讶:

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

foo() # bar

i = 'banana'

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

简而言之,请考虑动态:解释之前不会评估任何内容,这就是为什么您的代码使用m的最新值的原因。

当它在lambda执行中查找m时,m从最高范围获取,这意味着,正如其他人指出的那样;您可以通过添加另一个范围来解决该问题:

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

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

在这里,当调用lambda时,它将在lambda的定义范围内查找x。x是工厂主体中定义的局部变量。因此,lambda执行时使用的值将是在工厂调用期间作为参数传递的值。和多雷米!

值得注意的是,我可以将factory定义为factory(m)[x替换m],其行为是相同的。为了清楚起见,我使用了另一个名称:)

您可能会发现Andrej Bauer也遇到了类似的lambda问题。该博客上有趣的是评论,您将在其中详细了解python闭包:)


1

与当前问题并没有直接关系,但仍然是无价之宝:Fredrik Lundh撰写的Python Objects


1
与您的答案没有直接关系,而是搜索小猫:google.com/search?
单身

@Singletoned:如果OP盯着我提供的链接的文章,那么他们一开始就不会问这个问题;这就是为什么它是间接相关的。我敢肯定,您会很高兴向我解释小猫如何与我的答案间接相关(我想是通过一种整体方法;)
tzot 2011年

1

是的,这是范围的问题,无论您使用的是lambda还是局部函数,它都绑定到外部m。相反,请使用函子:

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

lambda的解决方案更多是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']

外部lambda用于将的当前值绑定ij

每次lambda调用外部函数时,都会创建一个内部实例,lambdaj绑定到ias的当前i


0

首先,您看到的不是问题,并且与按引用调用或按值调用无关。

您定义的lambda语法没有参数,因此,您使用参数看到的范围 m在lambda函数外部。这就是为什么您看到这些结果的原因。

在您的示例中,Lambda语法不是必需的,您宁愿使用一个简单的函数调用:

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

同样,您应该非常精确地了解正在使用的lambda参数以及它们的作用域确切的开始和结束位置。

作为附带说明,关于参数传递。python中的参数始终是对对象的引用。引用Alex Martelli的话:

术语问题可能是由于以下事实:在python中,名称的值是对对象的引用。因此,您始终传递值(不隐式复制),并且该值始终是引用。[...]现在,如果您想为此命名,例如“按对象引用”,“按未复制的值”或诸如此类,请成为我的客人。恕我直言,试图重用通常用于“变量是盒子”的语言到“变量是便利标签”的语言的术语,恕我直言,这很可能使人困惑而不是有所帮助。


0

该变量m已被捕获,因此您的lambda表达式始终会看到其“当前”值。

如果需要及时有效地捕获值,则编写一个函数,将所需的值作为参数,然后返回一个lambda表达式。此时,lambda将捕获参数的值,当您多次调用该函数时,该值不会更改:

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

输出:

do
re
mi

0

在Python中,实际上并没有经典意义上的变量,只有已经由对适用对象的引用绑定的名称。甚至函数也是Python中的某种对象,而lambda不会成为规则的例外:)


当您说“经典意义上”时,您的意思是“就像C一样。” 语言,包括Python充足,实现变量不同于C.
斯内德尔德

0

顺便提一句map,,尽管被一些著名的Python人物所鄙视,但它强制构建了防止这种陷阱的结构。

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

注意:lambda i在其他答案中,第一个行为类似于工厂。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.