156

当我在调查 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”。

4

10 回答 10

159

Python 实际上按照定义的方式运行。创建了三个单独的函数,但它们每个都有定义它们的环境的闭包- 在这种情况下,全局环境(如果循环放置在另一个函数中,则为外部函数的环境)。然而,这正是问题所在 - 在这种环境中,i 是 mutated,并且闭包都引用相同的 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)

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

于 2008-10-24T14:47:08.953 回答
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)
于 2008-10-25T01:56:42.237 回答
36

这是使用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。

于 2011-07-24T06:24:40.130 回答
14

看这个:

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))
于 2008-10-24T14:36:07.493 回答
8

正在发生的事情是变量 i 被捕获,并且函数正在返回它在调用时绑定的值。在函数式语言中,这种情况永远不会出现,因为 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))
))

看看here以进一步讨论这个问题。

[编辑] 描述它的更好方法可能是将 do 循环视为执行以下步骤的宏:

  1. 定义一个采用单个参数 (i) 的 lambda,其主体由循环主体定义,
  2. 使用适当的 i 值作为参数立即调用该 lambda。

IE。相当于下面的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 变量。

于 2008-10-25T11:28:04.720 回答
4

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

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

>>> t[1](2)
2
>>> t[2](2)
4
于 2008-10-24T14:42:55.607 回答
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 阵营。这里似乎有一个实现决定,不同的语言以不同的方式处理。我很想知道这个决定是什么,确切地说。

于 2008-10-25T07:20:03.847 回答
2

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

于 2008-10-24T14:17:35.143 回答
1

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

于 2012-07-24T08:20:04.593 回答
0

我不喜欢上面的解决方案是如何wrappers在循环中创建的。注意:python 3.xx

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)
于 2020-11-18T14:38:00.757 回答