词法闭包是如何工作的?

161 投票
10 回答
38757 浏览
提问于 2025-04-11 09:35

我在研究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)

这就是当你把副作用和函数式编程混在一起时会发生的事情。

撰写回答