Python lambda闭包作用域

35

我试图使用闭包从函数签名中消除一个变量(应用是为了编写连接Qt信号以控制大量参数的界面所需的所有函数,将这些函数写入存储值的字典中)。

我不明白为什么在不使用另一个函数包装的情况下使用lambda会返回所有情况的最后一个名称。

names = ['a', 'b', 'c']

def test_fun(name, x):
    print(name, x)

def gen_clousure(name):
    return lambda x: test_fun(name, x)

funcs1 = [gen_clousure(n) for n in names]
funcs2 = [lambda x: test_fun(n, x) for n in names]

# this is what I want
In [88]: for f in funcs1:
   ....:     f(1)
a 1
b 1
c 1

# I do not understand why I get this
In [89]: for f in funcs2:
   ....:     f(1)
c 1
c 1
c 1
1个回答

67

闭包(lambda或其他方式)的原因是它们关闭名称,而不是值。 当您定义lambda x:test_fun(n,x)时,n不会被评估,因为它在函数内部。 在调用函数时才会评估它,此时存在的值是循环中的最后一个值。

您一开始说想要“使用闭包从函数签名中消除变量”,但实际上并不是这样。 (但请看下面,如果您是指“消除”什么,可能有一种方法可以满足您。)函数体内的变量在定义函数时不会被评估。 为了使函数获取变量在函数定义时的“快照”,必须将变量作为参数传递。 通常的做法是给函数一个默认值为外部作用域变量的参数。 看一下这两个示例之间的区别:

>>> stuff = [lambda x: n+x for n in [1, 2, 3]]
>>> for f in stuff:
...     print f(1)
4
4
4
>>> stuff = [lambda x, n=n: n+x for n in [1, 2, 3]]
>>> for f in stuff:
...     print f(1)
2
3
4
在第二个例子中,将 n 作为参数传递给函数“锁定”了该函数的当前值。如果您想以这种方式锁定该值,必须执行此操作。(如果不是这样,像全局变量之类的东西根本无法工作;自由变量在使用时查找是至关重要的。)
请注意,这种行为与 lambda 表达式无关。如果您使用 def 定义引用封闭作用域中的变量的函数,则会产生相同的作用域规则。
如果确实希望如此,可以避免向返回的函数添加额外的参数,但必须将该函数包装在另一个函数中,如下所示:
>>> def makeFunc(n):
...     return lambda x: x+n
>>> stuff = [makeFunc(n) for n in [1, 2, 3]]
>>> for f in stuff:
...     print f(1)
2
3
4

在这里,内层lambda函数仍然在调用时查找n的值。但是它所引用的n不再是一个全局变量,而是封闭函数makeFunc内部的一个局部变量。每次调用makeFunc时都会创建一个新的此局部变量的值,并返回的lambda函数创建了一个闭包,"保存"了该调用makeFunc时有效的局部变量值。因此,在循环中创建的每个函数都有自己的名为x的"私有"变量。(对于这个简单的情况,这也可以使用外层lambda函数来完成——stuff = [(lambda n: lambda x: x+n)(n) for n in [1, 2, 3]] —— 但这样可读性较差。)

请注意,您仍然必须将n作为参数传递,只是通过这种方式,您不会将其作为参数传递给最终进入stuff列表的同一函数;相反,您将其作为参数传递给一个帮助函数,该函数创建您要放入stuff中的函数。使用这种两个函数的方法的优点是返回的函数是"干净的",没有多余的参数;如果你要包装接受很多参数的函数,这可能很有用,在这种情况下,记住n参数在列表中的位置可能会变得混乱。缺点是通过这种方式,制作函数的过程更加复杂,因为您需要另一个封闭函数。

总之,存在一种权衡:你可以使函数创建过程更简单(即,不需要两个嵌套的函数),但此时你必须让生成的函数变得稍微复杂一些(即,它有这个额外的n=n参数)。或者你可以使函数更简单(即,它没有n=n参数),但此时你必须让生成函数的过程变得更加复杂(即,你需要两个嵌套的函数来实现机制)。


这篇文章对于Python的这种行为的解释比那些更受欢迎的类似问题的答案要好得多。 - z33k

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接