当我在研究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 * 

当我在研究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代码是正确的:

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)

" 0 2 4"打印出来。

你能解释一下这个区别吗?


更新:

问题不是 i 是全球性的。这显示相同的行为:

<*>

如注释行所示,此处 i 未知。仍然,它打印“4 4 4”。

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

&quot; 0 2 4&quot;打印出来。

你能解释一下这个区别吗?


更新:

问题不是 i 是全球性的。这显示相同的行为:

<*>

如注释行所示,此处 i 未知。仍然,它打印“4 4 4”。

有帮助吗?

解决方案

Python实际上是按照定义的行为。 创建了三个单独的函数,但它们各自具有中定义的环境的闭包 - 在这种情况下,全局环境(或外部函数的环境,如果循环放在另一个函数内)。这确实是问题所在 - 在这种环境中,我是变异的,所有的闭包都指的是同一个

这是我能想到的最佳解决方案 - 创建一个函数创建器并调用 。这将为每个创建的函数强制不同的环境,每个函数都有不同的

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 并使用它的值,一个常见的模式是将其设置为参数default:参数默认值在执行 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)

以下是使用 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>,)

这意味着它们都指向同一个i变量实例,一旦循环结束,它将具有值2.

可读解决方案:

for i in xrange(3):
        def ffunc(i):
            def func(x): return x * i
            return func
        flist.append(ffunc(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))
))

请参阅此处,以进一步讨论此问题。

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

  1. 使用单个参数(i)定义一个lambda,其中一个主体由循环体定义,
  2. 使用适当的i值作为参数立即调用该lambda。
  3. 即。相当于下面的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变量。

我仍然不完全相信为什么在某些语言中这种方式可以单向运行,也可以用其他方式运行。在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阵营中。看来这里有一个实施决策,不同的语言以不同的方式处理。我很想知道究竟是什么决定。

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

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

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

变量 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上调用函数时,没有任何改变代码>

已经解释了行为背后的原因,并且已经发布了多个解决方案,但我认为这是最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”)。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top