词法闭包是如何工作的?

当我在研究 Javascript 代码中的 lexical 闭包问题时,我在 Python 中遇到了这个问题:

flist = []


for i in xrange(3):
def func(x): return x * i
flist.append(func)


for f in flist:
print f(2)

注意,这个例子小心地避开了 lambda。它打印“444”,这是令人惊讶的。我期望“024”。

这个等价的 Perl 代码做得很好:

my @flist = ();


foreach my $i (0 .. 2)
{
push(@flist, sub {$i * $_[0]});
}


foreach my $f (@flist)
{
print $f->(2), "\n";
}

“024”打印出来了。

你能解释一下这两者的区别吗?


更新:

问题 不是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在这一点上是未知的。但是,它打印“444”。

37605 次浏览

变量 i是一个全局变量,每次调用函数 f时它的值为2。

我会倾向于实施你们要的行为如下:

>>> class f:
...  def __init__(self, multiplier): self.multiplier = multiplier
...  def __call__(self, multiplicand): return self.multiplier*multiplicand
...
>>> flist = [f(i) for i in range(3)]
>>> [g(2) for g in flist]
[0, 2, 4]

响应你的更新 : 并不是 i 本质上的全局性导致了这种行为,而是因为它是一个封闭范围中的变量,在调用 f 的时候,它有一个固定的值。在第二个示例中,i的值取自 kkk函数的作用域,当您调用 flist上的函数时,这一点没有任何改变。

看看这个:

for f in flist:
print f.func_closure




(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)

这意味着它们都指向同一个 i 变量实例,一旦循环结束,该实例的值将为2。

一个可读的解决方案:

for i in xrange(3):
def ffunc(i):
def func(x): return x * i
return func
flist.append(ffunc(i))

问题是所有本地函数都绑定到相同的环境,因此绑定到相同的 i变量。解决方案(变通方法)是为每个函数(或 lambda)创建单独的环境(堆栈帧) :

t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]


>>> t[1](2)
2
>>> t[2](2)
4

Python 实际上是按照定义的方式运行的。创建了 三个独立的功能,但是它们都有 关闭他们所定义的环境-在本例中是全局环境(如果循环放在另一个函数中,则是外部函数的环境)。但是,这正是问题所在——在这个环境中,我被改造了和闭包都是 指同一个 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)

这就是将副作用和函数式编程混合在一起时发生的情况。

循环中定义的函数在同一变量 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)

我仍然不能完全相信为什么在某些语言中,这种方法是以一种方式工作的,而在另一种方式工作的。在 Common Lisp 中,它类似于 Python:

(defvar *flist* '())


(dotimes (i 3 t)
(setf *flist*
(cons (lambda (x) (* x i)) *flist*)))


(dolist (f *flist*)
(format t "~a~%" (funcall f 2)))

打印“666”(注意,这里的列表是从1到3,并且是反向构建的)。 在 Scheme 中,它的工作原理与 Perl 类似:

(define flist '())


(do ((i 1 (+ 1 i)))
((>= i 4))
(set! flist
(cons (lambda (x) (* i x)) flist)))


(map
(lambda (f)
(printf "~a~%" (f 2)))
flist)

打印“642”

正如我已经提到的,Javascript 属于 Python/CL 阵营。这里似乎有一个实现决策,不同的语言以不同的方式处理这个决策。我很想知道到底是什么决定。

所发生的情况是,变量 i 被捕获,函数在调用时返回它绑定到的值。在函数式语言中,这种情况从来不会出现,因为我不会反弹。但是,对于 python,以及您在 lisp 中看到的情况,这种情况已经不再正确。

您的方案示例的不同之处在于 do 循环的语义。Scheme 在循环中每次都有效地创建一个新的 i 变量,而不是像其他语言那样重用现有的 i 绑定。如果您使用在循环外部创建的另一个变量并对其进行变异,您将在 schema 中看到相同的行为。尝试将循环替换为:

(let ((ii 1)) (
(do ((i 1 (+ 1 i)))
((>= i 4))
(set! flist
(cons (lambda (x) (* ii x)) flist))
(set! ii i))
))

请看一下 给你,以便进一步讨论这个问题。

[编辑]可能更好的描述方式是把 do 循环想象成一个执行以下步骤的宏:

  1. 定义一个带有单个参数(i)的 lambda,由循环体定义一个主体,
  2. 对该 lambda 的即时调用,其参数为适当的 i 值。

相当于下面的蟒蛇:

flist = []


def loop_body(i):      # extract body of the for loop to function
def func(x): return x*i
flist.append(func)


map(loop_body, xrange(3))  # for i in xrange(3): body

I 不再是来自父范围的变量,而是它自己范围内的一个全新变量(即。Lambda 的参数) ,所以你得到你观察到的行为。Python 没有这个隐式的新作用域,所以 for 循环的主体只是共享 i 变量。

下面是如何使用 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))

输出024,如预期。

这种行为背后的原因已经解释过了,并且已经发布了多个解决方案,但是我认为这是最 Python 化的(记住,Python 中的所有东西都是一个对象!):

flist = []


for i in xrange(3):
def func(x): return x * func.i
func.i=i
flist.append(func)


for f in flist:
print f(2)

Claudiu 的答案非常好,使用了函数生成器,但是 piro 的答案是一个黑客,老实说,因为它将 i 变成了一个带有默认值的“隐藏”参数(它会工作得很好,但是它不是“ pythonic”)。

我不喜欢上面的解决方案是如何在循环中创建 wrappers

flist = []


def func(i):
return lambda x: x * i


for i in range(3):
flist.append(func(i))


for f in flist:
print f(2)