Python (lambda)函数闭包捕获什么?

Python (lambda)函数闭包捕获什么?,python,lambda,closures,Python,Lambda,Closures,最近我开始玩弄Python,我发现闭包的工作方式有点奇怪。考虑下面的代码: adders=[None, None, None, None] for i in [0,1,2,3]: adders[i]=lambda a: i+a print adders[1](3) x = "foo" def print_x(): print x x = "bar" print_x() # Outputs "bar" 它构建了一个简单的函数数组,这些函数接受单个输入并返回由数字相加的输

最近我开始玩弄Python,我发现闭包的工作方式有点奇怪。考虑下面的代码:

adders=[None, None, None, None]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)
x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"
它构建了一个简单的函数数组,这些函数接受单个输入并返回由数字相加的输入。这些函数是在for循环中构造的,其中迭代器i从0运行到3。对于这些数字中的每一个,将创建一个lambda函数,该函数捕获i并将其添加到函数的输入中。最后一行使用3作为参数调用第二个lambda函数。令我惊讶的是,产量是6

我期待一个4。我的推理是:在Python中,一切都是对象,因此每个变量都是指向它的指针。在为i创建lambda闭包时,我希望它存储指向i当前指向的整数对象的指针。这意味着当我分配一个新的integer对象时,它不应该影响以前创建的闭包。遗憾的是,在调试器中检查加法器数组表明它确实如此。所有lambda函数都引用i,3的最后一个值,这导致加法器[1]3返回6

这让我想知道以下几点:

闭包到底捕获了什么? 说服lambda函数捕获i的当前值的最优雅的方法是什么?这种方法在i更改其值时不会受到影响?
在回答第二个问题时,最优雅的方法是使用接受两个参数的函数,而不是数组:

add = lambda a, b: a + b
add(1, 3)
然而,在这里使用lambda有点愚蠢。Python为我们提供了操作符模块,它为基本操作符提供了一个功能接口。上面的lambda有不必要的开销,只是为了调用加法运算符:

from operator import add
add(1, 3)
我知道你在玩弄,试图探索这门语言,但我无法想象我会使用一系列函数,而Python的作用域奇怪会阻碍我

如果需要,可以编写一个使用数组索引语法的小型类:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)

你的第二个问题已经得到了回答,但关于你的第一个问题:

闭包到底捕获了什么

Python中的作用域是动态的和词法的。闭包将始终记住变量的名称和范围,而不是它所指向的对象。由于示例中的所有函数都是在相同的范围内创建的,并且使用相同的变量名,因此它们总是引用相同的变量

编辑:关于如何克服这个问题的另一个问题,我想到了两种方法:

最简洁,但不是严格等价的方法是。创建带有额外参数的lambda,并将额外参数的默认值设置为要保留的对象

每次创建lambda时,创建一个新的作用域会更详细一些,但不那么麻烦:

>>> adders = [0,1,2,3]
>>> for i in [0,1,2,3]:
...     adders[i] = (lambda b: lambda a: b + a)(i)
...     
>>> adders[1](3)
4
>>> adders[2](3)
5
为简洁起见,这里的作用域是使用新函数lambda创建的,该函数绑定其参数,并将要绑定的值作为参数传递。但是,在实际代码中,您很可能会使用普通函数而不是lambda来创建新的作用域:

def createAdder(x):
    return lambda y: y + x
adders = [createAdder(i) for i in range(4)]

可以使用具有默认值的参数强制捕获变量:

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

其思想是声明一个巧妙命名为i的参数,并为其提供一个要捕获i值的变量的默认值考虑以下代码:

adders=[None, None, None, None]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)
x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"
我想大多数人都不会觉得这让人困惑。这是预期的行为

那么,为什么人们认为在循环中进行时会有所不同呢?我知道我自己犯了那个错误,但我不知道为什么。是环路吗?或者是兰姆达

毕竟,循环只是以下内容的一个较短版本:

adders= [0,1,2,3]
i = 0
adders[i] = lambda a: i+a
i = 1
adders[i] = lambda a: i+a
i = 2
adders[i] = lambda a: i+a
i = 3
adders[i] = lambda a: i+a

为了完整性起见,第二个问题的另一个答案是:您可以在模块中使用

按照Chris Lutz的建议,通过从运算符导入add,示例变为:

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
   # store callable object with first argument given as (current) i
   adders[i] = partial(add, i) 

print adders[1](3)

下面是一个新的示例,它突出显示了闭包的数据结构和内容,以帮助澄清何时保存封闭上下文

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()
什么是闭包

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

对i的作用域进行分类的一种方法是在另一个作用域中生成lambda作为闭包函数,并为其传递必要的参数,以使lambda:

>>> adders = [0,1,2,3]
>>> for i in [0,1,2,3]:
...     adders[i] = (lambda b: lambda a: b + a)(i)
...     
>>> adders[1](3)
4
>>> adders[2](3)
5
def get_funkyi: 返回λa:i+a 加法器=[无,无,无,无] 对于[0,1,2,3]中的i: 加法器[i]=get_funkyi 加法器中ar的打印*ar5
当然给5 6 7 8。

Chris,当然上面的代码与我原来的问题无关。这是为了用一种简单的方式来说明我的观点。这当然是毫无意义和愚蠢的。这是循环,因为在许多其他语言中,循环可以创建新的作用域。这个答案很好,因为它解释了为什么每个lambda函数都访问相同的i变量。我在UI代码中遇到过这个问题。把我逼疯了。诀窍是要记住循环不会创建新的作用域。@TimMB我如何离开名称空间?@detly好吧,我想说的是,在循环之后,我不会工作。但我自己测试了一下
现在我明白你的意思了——它确实有效。我不知道循环变量在python中的循环体之后仍然存在。@TimMB-是的,这就是我的意思。if、with、try等也一样。这在官方Python常见问题解答的下,有一个解释和常用的解决方法。Python有静态作用域,而不是动态作用域。。只是所有变量都是引用,所以当你把一个变量设置为一个新对象时,变量本身,引用有相同的位置,但它指向其他的东西。如果设置!则在Scheme中也会发生同样的情况!。请参见此处了解动态范围的真正含义:。选项2类似于函数语言将调用的Curried函数。+1用于使用默认值。在定义lambda时进行评估使其非常适合此用途。+1还因为这是由认可的解决方案。这太神奇了。但是,Python的默认行为不是。这似乎不是一个好的解决方案。。。实际上,更改函数签名只是为了捕获变量的副本。调用该函数的人也会弄乱i变量,对吗?@DavidCallanan我们说的是lambda:一种特殊的函数,通常在自己的代码中定义以填补漏洞,而不是通过整个sdk共享。如果需要更强的签名,则应使用实函数。注意,[address X]>处的输出int对象使我认为闭包正在存储[address X]又名引用。但是,如果在lambda语句之后重新分配变量,[address X]将更改。