【问题标题】:How do lexical closures work?词法闭包是如何工作的?
【发布时间】:2010-09-19 00:29:00
【问题描述】:

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

【问题讨论】:

标签: python closures lazy-evaluation late-binding python-closures


【解决方案1】:

Python 实际上按照定义的方式运行。 创建了三个单独的函数,但它们每个都有定义它们的环境的闭包 - 在这种情况下,全局环境(或外部函数的环境,如果循环被放置在另一个函数中)。这正是问题所在 - 在这种环境中,i 发生了变异,并且所有闭包都引用了相同的 i

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

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

【讨论】:

  • 您的解决方案也是Javascript中使用的解决方案。
  • 这不是不当行为。它的行为与定义完全一致。
  • IMO piro 有更好的解决方案stackoverflow.com/questions/233673/…
  • 为了清楚起见,我可能会将最里面的“i”更改为“j”。
  • 像这样定义它怎么样:def inner(x, i=i): return x * i
【解决方案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)

【讨论】:

  • s/编译时/def语句执行时/
  • 这是一个巧妙的解决方案,结果很糟糕。
  • 这个解决方案有一个问题:func 现在有两个参数。这意味着它不适用于可变数量的参数。更糟糕的是,如果您使用第二个参数调用 func,这将覆盖定义中的原始 i。 :-(
【解决方案3】:

这是使用 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.反正有这个吗?
  • 当然。假设您有一个 Math 类,其方法是 add(self, a, b),并且您想设置 a=1 来创建“增量”方法。然后,为您的类“my_math”创建一个实例,您的增量方法将是“increment = partial(my_math.add, 1)”。
  • 要将此技术应用于一种方法,您也可以使用 functools.partialmethod() 从 python 3.4 开始
【解决方案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))

【讨论】:

  • 我的问题更“笼统”。为什么 Python 有这个缺陷?我希望支持词法闭包的语言(如 Perl 和整个 Lisp 王朝)能够正确解决这个问题。
  • 问为什么有缺陷是假设它不是缺陷。
【解决方案5】:

发生的情况是变量 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。

即。相当于下面的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 循环的语义差异。谢谢
【解决方案6】:

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

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

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

【讨论】:

    【解决方案7】:

    我仍然不完全相信为什么在某些语言中这是一种方式,而在另一种方式中。在 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 阵营。这里似乎有一个实现决策,不同的语言以不同的方式处理。我很想知道究竟是什么决定。

    【讨论】:

    • 不同之处在于 (do ...) 而不是范围规则。在方案中,do 每次通过循环都会创建一个新变量,而其他语言则重用现有的绑定。有关更多详细信息以及与 lisp/python 行为相似的方案版本示例,请参阅我的答案。
    【解决方案8】:

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

    【讨论】:

      【解决方案9】:

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

      【讨论】:

      • 我认为这取决于您的 python 版本。现在我更有经验了,我不会再建议这样做了。 Claudiu 是在 Python 中进行闭包的正确方法。
      • 这在 Python 2 或 3 上都不起作用(它们都输出“4 4 4”)。 x * func.i 中的 func 将始终引用最后定义的函数。因此,即使每个函数都单独粘贴了正确的数字,但它们最终都会从最后一个读取。
      【解决方案10】:

      我不喜欢上面的解决方案如何在循环中创建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)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-01-06
        • 2013-06-07
        • 2012-09-08
        • 1970-01-01
        • 1970-01-01
        • 2017-07-04
        相关资源
        最近更新 更多