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

Boa*_*oaz 237 python lambda closures

最近我开始玩Python,我遇到了一些特殊的闭包方式.请考虑以下代码:

adders=[0,1,2,3]

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

print adders[1](3)
Run Code Online (Sandbox Code Playgroud)

它构建了一个简单的函数数组,它接受单个输入并返回由数字添加的输入.函数在for循环中构造,迭代器i从中循环03.对于这些数字中的每一个,lambda都会创建一个函数i,该函数捕获并将其添加到函数的输入中.最后一行将第二个lambda函数3作为参数调用.令我惊讶的是输出结果是6.

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

这让我想知道以下内容:

  • 闭包捕获的内容是什么?
  • 什么是最优雅的方式来说服lambda函数以更改其值i时不会受到影响的方式捕获当前i值?

Adr*_*son 186

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

>>> 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
Run Code Online (Sandbox Code Playgroud)

我们的想法是声明一个参数(巧妙地命名i)并给它一个你想要捕获的变量的默认值(值 i)

  • +1也因为这是[官方常见问题解答]认可的解决方案(https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-不同的值,所有回报最相同的结果). (19认同)
  • **这太棒了.**然而,默认的Python行为不是. (17认同)
  • 使用默认值+1.在定义lambda时进行评估使它们非常适合这种用途. (7认同)
  • 但这似乎不是一个好的解决方案......您实际上只是为了捕获变量的副本而更改函数签名。而且那些调用该函数的人也可能会弄乱 i 变量,对吧? (7认同)
  • @DavidCallanan,我们谈论的是 lambda:一种您通常在自己的代码中定义的临时函数,用于填补漏洞,而不是通过整个 sdk 共享的东西。如果你需要更强的签名,你应该使用真实的函数。 (6认同)

Max*_*keh 150

你的第二个问题已得到解答,但至于你的第一个问题:

封闭捕获到底是什么?

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

编辑:关于如何克服这个问题的另一个问题,有两种方法可以想到:

  1. 最简洁但不严格等同的方式是Adrien Plisson推荐的方式.使用额外参数创建lambda,并将额外参数的默认值设置为要保留的对象.

  2. 每次创建lambda时,创建一个新范围会更冗长但更少hacky:

    >>> 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
    
    Run Code Online (Sandbox Code Playgroud)

    这里的范围是使用一个新函数(lambda,为简洁起见)创建的,它绑定了它的参数,并传递你想绑定的值作为参数.但是,在实际代码中,您很可能会使用普通函数而不是lambda来创建新范围:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]
    
    Run Code Online (Sandbox Code Playgroud)

  • 选项2类似于函数式语言称之为"Curried函数". (5认同)
  • Python具有静态作用域,而不是动态作用域.它只是所有变量都是引用,因此当您将变量设置为新对象时,变量本身(引用)具有相同的位置,但它指向其他内容.如果你"设置!",那么在Scheme中会发生同样的事情.请参阅此处了解动态范围:http://www.voidspace.org.uk/python/articles/code_blocks.shtml. (3认同)

Jom*_*oma 32

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

通过从运营商导入添加,Chris Lutz提出的示例变为:

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)
Run Code Online (Sandbox Code Playgroud)


tru*_*ppo 21

请考虑以下代码:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"
Run Code Online (Sandbox Code Playgroud)

我想大多数人都不会发现这种混乱.这是预期的行为.

那么,为什么人们认为它在循环中完成时会有所不同?我知道我自己犯了这个错误,但我不知道为什么.这是循环?或者也许是lambda?

毕竟,循环只是一个较短的版本:

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
Run Code Online (Sandbox Code Playgroud)

  • 这是循环,因为在许多其他语言中,循环可以创建新的范围. (10认同)
  • 这个答案很好,因为它解释了为什么每个 lambda 函数都访问相同的“i”变量。 (2认同)

Jef*_*eff 6

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

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()
Run Code Online (Sandbox Code Playgroud)

闭包里有什么?

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

值得注意的是,my_str 不在 f1 的闭包中。

f2的闭包里有什么?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43
Run Code Online (Sandbox Code Playgroud)

请注意(从内存地址)两个闭包包含相同的对象。因此,您可以开始将 lambda 函数视为对作用域的引用。但是,my_str 不在 f_1 或 f_2 的闭包中,并且 i 不在 f_3 的闭包中(未显示),这表明闭包对象本身是不同的对象。

闭包对象本身是同一个对象吗?

>>> print f_1.func_closure is f_2.func_closure
False
Run Code Online (Sandbox Code Playgroud)