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

2024-11-18 08:41:00
admin
原创
16
摘要:问题描述:最近我开始研究 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当然是给予。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

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

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

内置subversion和git源码管理

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

免费试用