词汇闭包如何工作?


149

当我研究Java代码中的词法闭包问题时,我遇到了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”。



3
这是一篇关于此问题的不错的文章。me.veekun.com/blog/2011/04/24/gotcha-python-scoping-closures
updogliu 2012年

Answers:


151

实际上,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)

当您混合副作用和功能编程时,就会发生这种情况。


5
您的解决方案也是Javascript中使用的解决方案。
伊莱·班德斯基

9
这不是行为不端。它的行为完全符合定义。
亚历克斯考文垂

6
IMO piro有更好的解决方案stackoverflow.com/questions/233673/…–
jfs

2
为了清楚起见,我可能会将最里面的“ i”更改为“ j”。
eggyntax 2011年

7
怎么这样定义它:def inner(x, i=i): return x * i
dashesy

152

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

7
s /在编译时/在def执行语句时//
jfs

23
这是一个巧妙的解决方案,这使其变得可怕。
Stavros Korokithakis

这种解决方案有一个问题:func现在有两个参数。这意味着它不适用于可变数量的参数。更糟糕的是,如果使用第二个参数调用func,则会覆盖i定义中的原始参数。:-(
Pascal

34

使用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,如预期的那样。


我真的很想使用它,但是我的功能实际上是一个类方法,传递的第一个值是self。有没有办法解决?
Michael David Watson

1
绝对。假设您有一个带有方法add(self,a,b)的Math类,并且您想要设置a = 1来创建“ increment”方法。然后,创建类“ my_math”的实例,您的增量方法将为“ increment = partial(my_math.add,1)”。
Luca Invernizzi

2
要将这种技术应用于一种方法,您还可以使用functools.partialmethod()python 3.4
Matt Eding '18

13

看这个:

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))

1
我的问题更“笼统”。为什么Python有此缺陷?我希望支持词汇闭包的语言(如Perl和整个Lisp朝代)能够正确解决这一问题。
伊莱·班德斯基

2
问为什么某物有缺陷就意味着它不是缺陷。
Null303

7

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

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

(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. 以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

i不再是父作用域中的那个,而是它自己作用域中的一个全新变量(即lambda的参数),因此您可以观察到行为。Python没有这个隐式的新作用域,因此for循环的主体仅共享i变量。


有趣。我不知道do循环在语义上的区别。谢谢
Eli Bendersky

4

我仍然不完全相信为什么在某些语言中这会以一种方式在另一种方式下起作用。在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)))

打印“ 6 6 6”(请注意,这里的列表是从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)

打印“ 6 4 2”

正如我已经提到的那样,JavaScript处于Python / CL阵营中。似乎这里有一个实施决策,即不同的语言采用不同的方式进行处理。我很想知道到底是什么决定。


8
区别在于(do ...)而不是范围规则。在scheme中,每次通过循环时都会创建一个新变量,而其他语言则重用现有的绑定。请参阅我的答案以获取更多详细信息以及行为类似于Lisp / python的方案版本的示例。
布赖恩

2

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

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

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

1

该变量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


0

已经解释了该行为背后的原因,并发布了多种解决方案,但是我认为这是最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的答案是hack,因为它使我成为具有默认值的“隐藏”参数(可以正常工作,但不是“ pythonic”) 。


我认为这取决于您的python版本。现在我更有经验了,我不再建议这样做。Claudiu的使用Python进行封闭的正确方法。
darkfeline 2013年

1
这在Python 2或3上都不起作用(它们都输出“ 4 4 4”)。在funcx * func.i总是引用定义的最后一个函数。因此,即使每个功能都单独贴有正确的数字,但它们最终还是要从最后一个读取。
Lambda Fairy
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.