Python 词汇闭包是如何工作的?

Python 词汇闭包是如何工作的?,python,closures,lazy-evaluation,late-binding,python-closures,Python,Closures,Lazy Evaluation,Late Binding,Python Closures,当我调查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”,这是令人惊讶的。我希望是“024” 此等效的Perl代码正确地实现了这一点: my @flist = (); forea

当我调查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”,这是令人惊讶的。我希望是“024”

此等效的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”。

变量
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
上调用函数时,没有任何变化请参见以下内容:

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
变量。解决方案(变通方法)是为每个函数(或lambda)创建单独的环境(堆栈帧):


Python实际上是按照定义运行的。创建了三个独立的函数,但每个函数都有定义它们的环境的闭包——在本例中,是全局环境(如果循环放置在另一个函数中,则是外部函数的环境)。但这正是问题所在——在这种环境中,i发生了变异,所有闭包都引用相同的i

这里是我能想到的最好的解决方案——创建一个函数creater并调用它。这将为创建的每个函数强制使用不同的环境,每个函数中都有不同的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)

这就是当你混合了副作用和函数式编程时会发生的情况

循环中定义的函数在其值更改时不断访问同一变量
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)

我仍然不完全相信为什么在某些语言中,这是一种方式,而在另一种方式。在公共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”(请注意,这里的列表是从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,函数返回它绑定的值在函数式语言中,这种情况从未出现过,因为我不会反弹。但是在python中,正如您在lisp中看到的,这不再是事实

scheme示例的不同之处在于do循环的语义。scheme每次通过循环都会有效地创建一个新的i变量,而不是像其他语言一样重用现有的i绑定。如果使用在循环外部创建的不同变量并对其进行变异,您将在sche中看到相同的行为me.尝试用以下内容替换您的循环:

(let ((ii 1)) (
  (do ((i 1 (+ 1 i)))
      ((>= i 4))
    (set! flist 
      (cons (lambda (x) (* ii x)) flist))
    (set! ii i))
))
让我们看看关于这方面的进一步讨论

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

  • 使用单个参数(i)定义lambda,其中主体由循环主体定义
  • 以适当的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变量。

    下面介绍如何使用
    functools
    库进行操作(我不确定在提出问题时是否有)


    输出0 2 4,正如预期的那样。

    行为背后的原因已经解释过,并且发布了多种解决方案,但我认为这是最具Python风格的(记住,Python中的一切都是对象!):


    Claudiu的答案很好,使用了函数生成器,但说实话,piro的答案是一个黑客,因为它使i变成了一个带有默认值的“隐藏”参数(它可以正常工作,但不是“pythonic”)。

    我不喜欢上面的解决方案如何在循环中创建
    包装器

    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)
    

    我的问题更“一般”。为什么Python会有这个缺陷?我希望有一种支持词法闭包的语言(比如Perl和整个Lisp王朝)为了正确地解决这个问题。询问某些东西为什么有缺陷是假设它不是缺陷。您的解决方案也是Javascript中使用的解决方案。这不是错误行为。它的行为完全符合定义。IMO piro有一个更好的解决方案,为了清楚起见,我可能会将最内层的“I”更改为“j”。这样定义它怎么样:
    def inner(x,I=I):返回x*i
    s/在编译时/在执行
    def
    语句时/这是一个i
    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
    
    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)
    
    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)
    
    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)