How do lexical closures work?
在我调查javascript代码中的词法闭包问题时,我在python中发现了这个问题:
1 2 3 4 5 6 7 8 | flist = [] for i in xrange(3): def func(x): return x * i flist.append(func) for f in flist: print f(2) |
注意,这个例子谨慎地避免了
这个等价的Perl代码正确地做到了:
1 2 3 4 5 6 7 8 9 10 11 12 | my @flist = (); foreach my $i (0 .. 2) { push(@flist, sub {$i * $_[0]}); } foreach my $f (@flist) { print $f->(2)," "; } |
打印"0 2 4"。
你能解释一下区别吗?
更新:
问题不在于
1 2 3 4 5 6 7 8 9 10 11 12 | 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) |
正如注释行所示,
循环中定义的函数在其值改变的同时继续访问同一个变量
为了评估
预期的工作如下:
1 2 3 4 5 6 7 8 9 | 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) |
实际上,python的行为是定义好的。创建了三个独立的函数,但每个函数都有它们所定义的环境的闭包——在本例中是全局环境(如果循环放置在另一个函数中,则为外部函数的环境)。不过,这正是问题所在——在这种环境中,我是变异的,闭包都引用同一个i。
这里是我能想到的最好的解决方案——创建一个函数creater并调用它。这将为每个创建的函数强制使用不同的环境,每个函数中都有不同的i。
1 2 3 4 5 6 7 8 9 10 | 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) |
这就是将副作用和功能性编程混合在一起时会发生的情况。
以下是您如何使用
1 2 3 4 5 6 7 8 9 10 11 | from functools import partial flist = [] def func(i, x): return x * i for i in xrange(3): flist.append(partial(func, i)) for f in flist: print f(2) |
按预期输出0 2 4。
看看这个:
1 2 3 4 5 6 7 | 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。
可读的解决方案:
1 2 3 4 5 | for i in xrange(3): def ffunc(i): def func(x): return x * i return func flist.append(ffunc(i)) |
所发生的是捕获变量i,函数返回调用时绑定到的值。在函数语言中,这种情况永远不会出现,因为我不会反弹。然而,对于Python,以及您在Lisp中看到的那样,这不再是真的了。
与您的方案示例的区别在于do循环的语义。方案是通过循环每次有效地创建一个新的i变量,而不是像使用其他语言那样重用现有的i绑定。如果您使用在循环外部创建的不同变量并对其进行变异,那么您将在方案中看到相同的行为。尝试将循环替换为:
1 2 3 4 5 6 7 | (let ((ii 1)) ( (do ((i 1 (+ 1 i))) ((>= i 4)) (set! flist (cons (lambda (x) (* ii x)) flist)) (set! ii i)) )) |
请看这里,进一步讨论一下。
[编辑]描述它的更好方法可能是将do循环看作执行以下步骤的宏:
即相当于下面的python:
1 2 3 4 5 6 7 | 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变量。
我仍然不完全相信,为什么在某些语言中,这是以一种方式工作,而在另一种方式工作。在普通的Lisp中,它就像python:
1 2 3 4 5 6 7 8 | (defvar *flist* '()) (dotimes (i 3 t) (setf *flist* (cons (lambda (x) (* x i)) *flist*))) (dolist (f *flist*) (format t"~a~%" (funcall f 2))) |
打印"6 6 6"(请注意,这里的列表是从1到3的,并且是反向构建的)。在方案中,它的工作方式与Perl类似:
1 2 3 4 5 6 7 8 9 10 11 | (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) |
版画"6 4 4"
正如我已经提到的,JavaScript在python/cl阵营中。这里似乎有一个实现决策,不同的语言以不同的方式进行处理。我很想知道到底是什么决定。
问题是所有本地函数都绑定到相同的环境,从而绑定到相同的
1 2 3 4 5 6 | t = [ (lambda x: lambda y : x*y)(x) for x in range(5)] >>> t[1](2) 2 >>> t[2](2) 4 |
变量
我倾向于实现您所追求的行为,如下所示:
1 2 3 4 5 6 7 | >>> 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] |
对更新的响应:导致这种行为的不是
行为背后的推理已经解释过了,并且发布了多种解决方案,但我认为这是最Python式的(记住,Python中的一切都是一个对象!):
1 2 3 4 5 6 7 8 9 | 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的答案是一个黑客,说实话,因为它让我变成了一个"隐藏的"参数,并有一个默认值(它会很好地工作,但不是"pythonic")。