词法闭包是如何工作的?
我在研究JavaScript代码中的词法闭包问题时,发现了Python中的一个问题:
flist = []
for i in xrange(3):
def func(x): return x * i
flist.append(func)
for f in flist:
print f(2)
注意,这个例子特意没有使用lambda
。它输出的是“4 4 4”,这让人感到意外。我本以为应该是“0 2 4”。
而这段等效的Perl代码就正确了:
my @flist = ();
foreach my $i (0 .. 2)
{
push(@flist, sub {$i * $_[0]});
}
foreach my $f (@flist)
{
print $f->(2), "\n";
}
它输出的是“0 2 4”。
你能解释一下这两者之间的区别吗?
更新:
问题并不在于变量i
是全局的。这段代码显示了相同的行为:
flist = []
def outer():
for i in xrange(3):
def inner(x): return x * i
flist.append(inner)
outer()
#~ print i # commented because it causes an error
for f in flist:
print f(2)
正如注释的那行所示,i
在那时是未知的。尽管如此,它仍然输出“4 4 4”。
10 个回答
36
下面是如何使用 functools
库来实现这个功能的(我不太确定在提问时这个库是否已经可用)。
from functools import partial
flist = []
def func(i, x): return x * i
for i in range(3):
flist.append(partial(func, i))
for f in flist:
print(f(2))
输出结果是 0 2 4,正如预期的那样。
157
在循环中定义的函数一直在访问同一个变量 i
,而这个变量的值会不断变化。到循环结束时,所有的函数都指向同一个变量,这个变量里存的就是循环结束时的最后一个值。这就是例子中所描述的效果。
为了在使用 i
的值时能够正确评估它,常用的做法是把它设置为参数的默认值:参数的默认值是在 def
语句执行时计算的,这样就能“冻结”循环变量的值。
下面的代码就能按预期工作:
flist = []
for i in xrange(3):
def func(x, i=i): # the *value* of i is copied in func() environment
return x * i
flist.append(func)
for f in flist:
print f(2)
160
Python的行为其实是符合定义的。这里创建了三个独立的函数,但它们每个都有自己定义时的环境闭包——在这个例子中,就是全局环境(或者如果这个循环放在另一个函数里面,就是外部函数的环境)。问题就在于,这个环境中,i被修改了,而这些闭包都指向同一个i。
我想到的最佳解决方案是——创建一个函数生成器,然后调用那个函数。这样可以为每个创建的函数强制设置不同的环境,每个环境中都有不同的i。
flist = []
for i in xrange(3):
def funcC(j):
def func(x): return x * j
return func
flist.append(funcC(i))
for f in flist:
print f(2)
这就是当你把副作用和函数式编程混在一起时会发生的事情。