Python (lambda)函数闭包捕获什么?
最近我开始玩弄Python,我发现闭包的工作方式有点奇怪。考虑下面的代码: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" 它构建了一个简单的函数数组,这些函数接受单个输入并返回由数字相加的输
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
指向的整数对象的指针。这意味着当i
分配一个新的整数对象时,它不应该影响先前创建的闭包。遗憾的是,在调试器中检查加法器
数组表明它确实如此。所有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中的作用域是动态的和词法的。闭包将始终记住变量的名称和范围,而不是它所指向的对象。由于示例中的所有函数都是在相同的范围内创建的,并且使用相同的变量名,因此它们总是引用相同的变量 编辑:关于如何克服这一问题的另一个问题,我们想到了两种方法:
>>> 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)
下面是一个新的示例,它突出显示了闭包的数据结构和内容,以帮助澄清何时“保存”封闭上下文 什么是闭包
>>> 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_funky(i):
返回λa:i+a
加法器=[无,无,无,无]
对于[0,1,2,3]中的i:
加法器[i]=get_funky(i)
打印(*(加法器中ar的ar(5))
当然给出
5 6 7 8
。Chris,当然上面的代码与我原来的问题无关。这是为了用一种简单的方式来说明我的观点。这当然是毫无意义和愚蠢的。这是循环,因为在许多其他语言中,循环可以创建新的作用域。这个答案很好,因为它解释了为什么每个lambda函数访问相同的i
变量。我在UI代码中遇到了这个问题。把我逼疯了。诀窍是记住循环不会创建新的作用域。@TimMBi
如何离开名称空间?@detly好吧,我想说的是print i
在循环之后不起作用。但我自己测试过,现在我明白你的意思了——它确实有效。我不知道循环变量在python中的循环体之后仍然存在。@TimMB-是的,这就是我的意思。与if
、with
、try
等相同。这在官方Python常见问题解答的下面,有一个解释和常见的解决方法。Python有静态作用域,而不是动态作用域。。只是所有变量都是引用,所以当您将变量设置为新对象时,变量本身(引用)具有相同的位置