lambda 函数闭包捕获什么?[重复]

2024-11-18 08:41:00
admin
原创
149
摘要:问题描述:最近我开始研究 Python,发现闭包的工作方式有些奇怪。考虑以下代码:adders = [None, None, None, None] for i in [0, 1, 2, 3]: adders[i] = lambda a: i+a print adders[1](3) 它构建了一个简...

问题描述:

最近我开始研究 Python,发现闭包的工作方式有些奇怪。考虑以下代码:

adders = [None, None, None, None]

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

print adders[1](3)

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

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

这让我对以下问题产生了疑问:

  • 闭包到底捕获了什么?

  • 有什么最优雅的方法可以说服lambda函数捕获当前值,i以便在i其值发生变化时不会受到影响?


有关该问题的更易于理解、更实用的版本,具体到使用循环(或列表推导、生成器表达式等)的情况,请参阅在循环(或推导)中创建函数(或 lambda)。这个问题的重点是理解 Python 中代码的底层行为。

如果您来这里是为了尝试解决在 Tkinter 中制作按钮的问题,请尝试在 tkinter 循环中创建按钮并传递命令参数以获得更具体的建议。

有关 Python如何实现闭包的技术细节,请参阅obj.__closure__ 中究竟包含什么? 。有关相关术语讨论,请参阅早期绑定和晚期绑定之间的区别是什么? 。


解决方案 1:

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

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

解决方案 2:

闭包到底捕获了什么?

Python 中的闭包使用词法作用域:它们记住创建闭包变量的名称和作用域。但是,它们仍然是后期绑定:在使用闭包中的代码时查找名称,而不是在创建闭包时查找。由于示例中的所有函数都是在同一作用域中创建的,并使用相同的变量名,因此它们始终引用相同的变量。

至少有两种方法可以获得早期绑定:

  1. 最简洁但不完全等价的方法是Adrien Plisson 推荐的方法。创建一个带有额外参数的 lambda,并将额外参数的默认值设置为要保留的对象。

  2. 更详细但也更健壮的是,我们可以为每个创建的 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)]

解决方案 3:

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

按照 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)

解决方案 4:

考虑以下代码:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

我想大多数人都不会觉得这令人困惑。这是预期的行为。

那么,为什么人们认为在循环中执行时会有所不同呢?我知道我自己也犯了这个错误,但我不知道为什么。是循环吗?或者是 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

解决方案 5:

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

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()

闭包里有什么?

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

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

f2 的闭包里有什么?

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

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

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

>>> print f_1.func_closure is f_2.func_closure
False

解决方案 6:

回答你的第二个问题,最优雅的方法是使用一个接受两个参数而不是数组的函数:

add = lambda a, b: a + b
add(1, 3)

但是,在这里使用 lambda 有点愚蠢。Python 为我们提供了模块operator,它为基本运算符提供了一个函数接口。上面的 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)

解决方案 7:

在函数中创建加法器来捕获值:

def create_adder(i):
    return lambda a: i + a


if __name__ == '__main__':
    adders = [None, None, None, None]

    for i in [0, 1, 2, 3]:
        adders[i] = create_adder(i)

    print(adders[1](3))

解决方案 8:

理清范围的一种方法i是在另一个范围(闭包函数)中生成 lambda,并将必要的参数交给它以生成 lambda:

def get_funky(i):
    return lambda a: i+a

adders=[None, None, None, None]

for i in [0,1,2,3]:
   adders[i]=get_funky(i)

print(*(ar(5) for ar in adders))

5 6 7 8当然是给予。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1259  
  IPD(Integrated Product Development)流程管理作为一种先进的产品开发管理理念和方法,在提升企业创新能力方面发挥着至关重要的作用。它打破了传统产品开发过程中部门之间的壁垒,通过整合资源、优化流程,实现产品的快速、高效开发,为企业在激烈的市场竞争中赢得优势。IPD流程管理的核心概念IPD流程...
IPD流程中PDCP是什么意思   11  
  IPD(Integrated Product Development)流程管理作为一种先进的产品开发管理模式,旨在通过整合各种资源,实现产品的高效、高质量开发。在这一过程中,团队协作无疑是成功的关键。有效的团队协作能够打破部门壁垒,促进信息共享,提升决策效率,从而确保产品开发项目顺利推进。接下来,我们将深入探讨IPD流...
IPD培训课程   9  
  IPD(Integrated Product Development)研发管理体系作为一种先进的产品开发理念和方法,在众多企业中得到了广泛应用。它旨在打破部门壁垒,整合资源,实现产品开发的高效、协同与创新。在项目周期方面,IPD研发管理体系有着深远且多维度的影响,深入剖析这些影响,对于企业优化产品开发流程、提升市场竞争...
华为IPD流程   11  
  IPD(Integrated Product Development)流程管理是一种先进的产品开发管理模式,旨在通过整合企业的各种资源,实现产品的高效、高质量开发。它涵盖了从产品概念提出到产品退市的整个生命周期,对企业的发展具有至关重要的意义。接下来将详细阐述IPD流程管理的五个阶段及其重要性。概念阶段概念阶段是IPD...
IPD概念阶段   12  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用