代码之家  ›  专栏  ›  技术社区  ›  Eli Bendersky

词汇闭包是如何工作的?

  •  137
  • Eli Bendersky  · 技术社区  · 16 年前

    在我调查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)
    

    正如注释行所示, 在那一点上是未知的。不过,它还是打印了“4,4,4”。

    9 回复  |  直到 7 年前
        1
  •  142
  •   Claudiu    14 年前

    实际上,python的行为是定义好的。 三个独立的功能 是被创造出来的,但是他们每个人都有 关闭在其中定义的环境 -在这种情况下,是全局环境(如果循环被放置在另一个函数中,则是外部函数的环境)。这正是问题所在,尽管-在这种环境中, 我被变异了 关闭所有 参考相同的I .

    这里是我能想到的最好的解决方案-创建一个函数creater和invoke 那个 相反。这将迫使 不同的环境 对于创建的每个函数, 不同的我 在每一个。

    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)
    

    这就是将副作用和函数式编程混合在一起时会发生的情况。

        2
  •  151
  •   piro    16 年前

    循环中定义的函数将继续访问同一个变量 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)
    
        3
  •  26
  •   Luca Invernizzi    7 年前

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

        4
  •  13
  •   Null303    16 年前

    看看这个:

    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))
    
        5
  •  7
  •   Brian    16 年前

    发生的是捕获变量i,函数返回调用时绑定到的值。在函数语言中,这种情况永远不会出现,因为我不会反弹。然而,对于Python,以及您在Lisp中看到的那样,这不再是真的了。

    与您的方案示例的区别在于do循环的语义。方案是通过循环每次有效地创建一个新的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. 定义一个lambda,使用一个由循环体定义的参数(i),
    2. 立即调用该lambda,并将适当的值i作为其参数。

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

        6
  •  4
  •   Eli Bendersky    16 年前

    我仍然不完全相信,为什么在某些语言中,这是以一种方式工作,而在另一种方式工作。在普通的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的,并且是反向构建的)。 在方案中,它的工作方式与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 4”

    正如我已经提到的,JavaScript在python/cl阵营中。这里似乎有一个实现决策,不同的语言以不同的方式进行处理。我很想知道到底是什么决定。

        7
  •  2
  •   Rafał Dowgird    16 年前

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

    t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]
    
    >>> t[1](2)
    2
    >>> t[2](2)
    4
    
        8
  •  1
  •   Alex Coventry    16 年前

    变量 i 是全局的,每次函数的值为2 f 被称为。

    我倾向于实现您所追求的行为,如下所示:

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

    对更新的响应 这不是世界性的 本身 这导致了这种行为,事实上它是一个封闭范围中的变量,在调用f时,该范围具有固定值。在第二个示例中, 是从 kkk 函数,当您调用函数 flist .

        9
  •  0
  •   darkfeline    13 年前

    行为背后的推理已经被解释过,并且发布了多种解决方案,但我认为这是最蟒蛇式的(记住,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”)。