Python (lambda)函数闭包捕获什么?

Python (lambda)函数闭包捕获什么?,python,lambda,closures,Python,Lambda,Closures,最近我开始玩弄Python,我发现闭包的工作方式有点奇怪。考虑下面的代码: adders=[None, None, None, None] for i in [0,1,2,3]: adders[i]=lambda a: i+a print adders[1](3) x = "foo" def print_x(): print x x = "bar" print_x() # Outputs "bar" 它构建了一个简单的函数数组,这些函数接受单个输入并返回由数字相加的输

最近我开始玩弄Python,我发现闭包的工作方式有点奇怪。考虑下面的代码:

adders=[None, None, None, None]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)
x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"
它构建了一个简单的函数数组,这些函数接受单个输入并返回由数字相加的输入。函数在
for
循环中构造,迭代器
i
0
运行到
3
。对于这些数字中的每一个,将创建一个
lambda
函数,该函数捕获
i
,并将其添加到函数的输入中。最后一行使用
3
作为参数调用第二个
lambda
函数。令我惊讶的是,输出是
6

我期望一个
4
。我的推理是:在Python中,一切都是对象,因此每个变量都是指向它的指针。在为
i
创建
lambda
闭包时,我希望它存储指向当前由
i
指向的整数对象的指针。这意味着当
i
分配一个新的整数对象时,它不应该影响先前创建的闭包。遗憾的是,在调试器中检查
加法器
数组表明它确实如此。所有
lambda
函数都引用
i
3
的最后一个值,这导致
加法器[1](3)
返回
6

这让我想知道以下几点:

  • 闭包到底捕获了什么
  • 说服
    lambda
    函数捕获
    i
    的当前值的最优雅的方法是什么,这种方法在
    i
    更改其值时不会受到影响

在回答第二个问题时,最优雅的方法是使用带有两个参数的函数,而不是数组:

add = lambda a, b: a + b
add(1, 3)
然而,在这里使用lambda有点愚蠢。Python为我们提供了
操作符
模块,它为基本操作符提供了一个功能接口。上面的lambda有不必要的开销,只是为了调用加法运算符:

from operator import add
add(1, 3)
我知道你在玩弄,试图探索这门语言,但我无法想象我会使用一系列函数,而Python的作用域奇怪会阻碍我

如果需要,可以编写一个使用数组索引语法的小型类:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)

你的第二个问题已经得到了回答,但关于你的第一个问题:

闭包到底捕获了什么

Python中的作用域是动态的和词法的。闭包将始终记住变量的名称和范围,而不是它所指向的对象。由于示例中的所有函数都是在相同的范围内创建的,并且使用相同的变量名,因此它们总是引用相同的变量

编辑:关于如何克服这一问题的另一个问题,我们想到了两种方法:

  • 最简洁,但不是严格等价的方法是。创建带有额外参数的lambda,并将额外参数的默认值设置为要保留的对象

  • 每次创建lambda时,创建一个新的作用域会更详细一些,但不那么麻烦:

    >>> adders = [0,1,2,3]
    >>> for i in [0,1,2,3]:
    ...     adders[i] = (lambda b: lambda a: b + a)(i)
    ...     
    >>> adders[1](3)
    4
    >>> adders[2](3)
    5
    
    这里的作用域是使用一个新函数(为简洁起见,使用lambda)创建的,该函数绑定其参数,并将要绑定的值作为参数传递。但是,在实际代码中,您很可能会使用普通函数而不是lambda来创建新的作用域:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]
    

  • 可以使用具有默认值的参数强制捕获变量:

    >>> for i in [0,1,2,3]:
    ...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
    ...
    >>> print( adders[1](3) )
    4
    

    我们的想法是声明一个参数(巧妙地命名为
    i
    ),并为其提供要捕获的变量的默认值(
    i

    考虑以下代码:

    adders=[None, None, None, None]
    
    for i in [0,1,2,3]:
       adders[i]=lambda a: i+a
    
    print adders[1](3)
    
    x = "foo"
    
    def print_x():
        print x
    
    x = "bar"
    
    print_x() # Outputs "bar"
    
    我想大多数人都不会觉得这让人困惑。这是预期的行为

    那么,为什么人们认为在循环中进行时会有所不同呢?我知道我自己犯了那个错误,但我不知道为什么。是环路吗?或者是兰姆达

    毕竟,循环只是以下内容的一个较短版本:

    adders= [0,1,2,3]
    i = 0
    adders[i] = lambda a: i+a
    i = 1
    adders[i] = lambda a: i+a
    i = 2
    adders[i] = lambda a: i+a
    i = 3
    adders[i] = lambda a: i+a
    

    为了完整性起见,第二个问题的另一个答案是:您可以在模块中使用

    按照Chris Lutz的建议,通过从运算符导入add,示例变为:

    from functools import partial
    from operator import add   # add(a, b) -- Same as a + b.
    
    adders = [0,1,2,3]
    for i in [0,1,2,3]:
       # store callable object with first argument given as (current) i
       adders[i] = partial(add, i) 
    
    print adders[1](3)
    

    下面是一个新的示例,它突出显示了闭包的数据结构和内容,以帮助澄清何时“保存”封闭上下文

    什么是闭包

    >>> print f_1.func_closure, f_1.func_closure[0].cell_contents
    (<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 
    

    整理
    i
    范围的一种方法是在另一个范围(闭包函数)中生成lambda,并为其移交必要的参数,以使lambda:

    >>> adders = [0,1,2,3]
    >>> for i in [0,1,2,3]:
    ...     adders[i] = (lambda b: lambda a: b + a)(i)
    ...     
    >>> adders[1](3)
    4
    >>> adders[2](3)
    5
    
    def get_funky(i):
    返回λa:i+a
    加法器=[无,无,无,无]
    对于[0,1,2,3]中的i:
    加法器[i]=get_funky(i)
    打印(*(加法器中ar的ar(5))
    

    当然给出
    5 6 7 8

    Chris,当然上面的代码与我原来的问题无关。这是为了用一种简单的方式来说明我的观点。这当然是毫无意义和愚蠢的。这是循环,因为在许多其他语言中,循环可以创建新的作用域。这个答案很好,因为它解释了为什么每个lambda函数访问相同的
    i
    变量。我在UI代码中遇到了这个问题。把我逼疯了。诀窍是记住循环不会创建新的作用域。@TimMB
    i
    如何离开名称空间?@detly好吧,我想说的是
    print i
    在循环之后不起作用。但我自己测试过,现在我明白你的意思了——它确实有效。我不知道循环变量在python中的循环体之后仍然存在。@TimMB-是的,这就是我的意思。与
    if
    with
    try
    等相同。这在官方Python常见问题解答的下面,有一个解释和常见的解决方法。Python有静态作用域,而不是动态作用域。。只是所有变量都是引用,所以当您将变量设置为新对象时,变量本身(引用)具有相同的位置