词法closures如何工作?
当我调查Javascript代码中的词法closures的问题时,我在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”。
Python实际上是按照定义的。 创build了三个单独的函数 ,但是每个函数都具有closures的环境,在这种情况下,全局环境(如果循环放置在另一个函数内,则是外部函数的环境)。 然而,这正是问题所在 – 在这种环境下, 我发生了变化 ,closures都是指同一个i 。
这是我能想到的最好的解决scheme – 创build一个函数创build器,然后调用它 。 这将强制创build每个function的不同环境 ,每个function都有不同的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)
当你混合使用副作用和函数式编程时,会发生这种情况。
循环中定义的函数在其值改变时保持访问相同的variablesi
。 在循环结束时,所有的函数都指向同一个variables,这个variables保存着循环中的最后一个值:效果就是这个例子中的报告。
为了评估i
并使用它的值,常见的模式是将其设置为默认参数:在执行def
语句时计算参数的默认值,从而冻结循环variables的值。
以下工作如期:
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)
下面是你如何使用functools
库(我不知道在提出问题时可用)。
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。
看这个:
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>,)
这意味着它们都指向同一个variables实例,一旦循环结束,它将具有值2。
可读的解决scheme:
for i in xrange(3): def ffunc(i): def func(x): return x * i return func flist.append(ffunc(i))
发生的事情是variables我被捕获,函数返回它被调用时绑定的值。 在function语言中,这种情况从来不会出现,因为我不会被反弹。 然而,用python,也正如你用lisp所看到的,这已经不是真的了。
与您的scheme示例的区别在于do循环的语义。 Scheme每次都通过循环有效地创build一个新的ivariables,而不是像其他语言一样重用现有的i绑定。 如果您使用在循环外部创build的另一个variables并对其进行变异,您将在scheme中看到相同的行为。 尝试replace你的循环:
(let ((ii 1)) ( (do ((i 1 (+ 1 i))) ((>= i 4)) (set! flist (cons (lambda (x) (* ii x)) flist)) (set! ii i)) ))
看看这里进一步的讨论。
[编辑]描述它的一个更好的方法是将do循环看作是执行以下步骤的macros:
- 定义一个采用单个参数(i)的lambdaexpression式,由循环体定义一个body,
- 以适当的值i作为参数的那个lambda的立即调用。
即。 相当于下面的python:
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
我不再是从父范围,而是一个全新的variables在其自己的范围(即参数的lambda),所以你得到你观察到的行为。 Python没有这个隐式的新的作用域,所以for循环的主体只是共享我的variables。
我仍然不完全相信,为什么在某些语言中,这是以一种方式进行的,或者以另一种方式进行。 在Common Lisp中,它就像Python一样:
(defvar *flist* '()) (dotimes (i 3 t) (setf *flist* (cons (lambda (x) (* xi)) *flist*))) (dolist (f *flist*) (format t "~a~%" (funcall f 2)))
打印“6 6 6”(注意,这里的列表是从1到3,并build立在相反的“)。而在Scheme中,它的工作原理就像Perl:
(define flist '()) (do ((i 1 (+ 1 i))) ((>= i 4)) (set! flist (cons (lambda (x) (* ix)) flist))) (map (lambda (f) (printf "~a~%" (f 2))) flist)
打印“6 4 2”
正如我已经提到的,Javascript是在Python / CL阵营。 这里似乎有一个实施决定,不同的语言以不同的方式处理。 我很想明白什么是决定。
问题是,所有的本地函数绑定到相同的环境,从而相同的i
variables。 解决scheme(解决方法)是为每个函数(或lambda)创build单独的环境(堆栈框架):
t = [ (lambda x: lambda y : x*y)(x) for x in range(5)] >>> t[1](2) 2 >>> t[2](2) 4
variablesi
是一个全局variables,每次函数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]
对你的更新的响应 :这不是引起这种行为的本身的全局性,而是它是一个封闭范围的variables,它在调用f的时候具有固定的值。 在你的第二个例子中, i
的值取自kkk
函数的范围,当你在flist
上调用函数时没有任何变化。
行为背后的原因已经被解释了,并且已经发布了多个解决scheme,但是我认为这是最pythonic(请记住,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的回答是一个黑客,说实话,因为它使我成为一个默认值的“隐藏”参数(它可以正常工作,但不是“pythonic”) 。