闭包到底能做啥?它有着什么样的妙用?
2024-01-24 03:59:28
闭包(Closure)是函数式编程中一个非常重要且别具一格的概念,在计算机科学中备受推崇。它能提供一个稳定且长期的环境,确保栈 式数据在函数多次执行时不会丢失。因此,能够理解闭包的特性、优点与缺点,对掌握计算机科学至关重要。接下来,我们将共同探索闭包的妙用,深入理解这种奇妙的编程特性。
闭包的妙处:让栈式数据得以延续
谈及闭包,首先我们有必要理清楚栈式数据和函数式编程的脉络。
栈 式数据和函数式编程是计算机编程的基石。栈 式数据的本质是函数的局部数据,是函数自身的private。换言之,其生命周期仅限于所归属的函数。而一旦函数的执行结束,这些栈式数据也随之烟消云散。函数 式编程,则是以高阶函数、闭包和递归等技术为基石的编程方法。由于栈式数据不可复用,因此函数 式编程在项目执行中显得尤为重要,可以说构成了项目执行的灵魂所在。
闭包的概念,最早源自20世纪70年代。闭包的本质是:一个函数及其代码在运行时的环境一起构成一个闭包。也即是:内层函数可以引用到外层函数中的任何东西,包括函数、数据。这种特性,在函数 式编程中能派上很大用场,因为,依赖于它,函数就能够将数据保留至下次执行时再行使用。如此,我们便得以延续栈 式数据,使它们得以在下次函数运行时继续保持其原有状态。为此,我们可以来举个例子。假设,我们定义了一个顶层函数,名之为: calculate()
。我们设定: calculate()
被设计用来向列表追加一个数字,并将改增后的列表作为输出。
def calculate(initial_list):
"""计算列表的长度。
Args:
initial_list: 用以计算长度的列表。
Returns:
改增后列表的长度。
"""
# 计算初始长度并将其追加到列表
length = len(initial_list)
initial_list.append(length)
# 返回改增后的列表长度
return length
倘若把 calculate()
仅仅作为一个常规函数看待,那么,毫无疑问, length
会随着函数的执行而随之逝去。这样,我们便无 法得知所计算出的长度。然,依托于闭包,我们就能够将 length
保留于下次执行时再行使用,这一过程,我们能够通过设定内层函数来实现。
def calculate(initial_list):
"""计算列表的长度。
Args:
initial_list: 用以计算长度的列表。
Returns:
改增后列表的长度。
"""
# 定义一个闭包
def calculate_and_append(x):
"""将列表的长度作为后缀追加到列表。
Args:
x: 列表的某个元素
Returns:
改增后的列表。
"""
# 确保`calculate_and_append`能使用 `initial_list`
nonlocal initial_list
length = len(initial_list)
initial_list.append(length)
return x
# 返回具有闭包的 `calculate_and_append`
return calculate_and_append
那么,现在,我们能够创建一个通过这个闭包返回的 calculate_and_append()
来生成一个元素列表。
calculate = calculate([])
现在,我们便可以反复使用 calculate_and_append()
函数来增补 initial_list
的长度。
calculate_and_append(1) # [0, 1]
calculate_and_append(3) # [0, 1, 2, 3]
calculate_and_append(1) # [0, 1, 2, 3, 4]
如你所见,我们现在得以反复使用 calculate_and_append()
以长度为后缀不断扩充 initial_list
。这展示了闭包能帮助我们如何得以保留 length
,即便在函数 calculate()
多次执行时, length
仍能保持原有的值,这便得益于 calculate_and_append()
函数对 initial_list
的修改。
实际上,这只是闭包的妙用之一。闭包还有诸多其他妙处。利用闭包,我们能够:
- 确保在不同的函数执行周期中始终保持一致的栈 式数据。
- 充分利用函数 式编程的特性。
- 保留函数原有的代码,即便该函数执行已久,进而得以执行新内容。
- 剔除对
length
等局部private
的硬编码依赖,进而使函数代码更加精简易读。
得益于闭包,我们能够将局部 private
,即栈式数据,保留在函数执行的不同周期中,以便日后使用。可以说,闭包是计算机科学的一项必备技术。
闭包的妙用不止于此,倘若我们想要在函数多次执行时保持一组私有数据的稳定状态,我们就不得不依赖于闭包。例如,当我们设计一个利用 calculate()
来计算一组数字列表长度的函数,那么,每当 calculate()
被执行,我们都能够利用 calculate_and_append()
来保留 length
以备日后使用。如此,我们就不必将 length
硬编码至函数之中,进而将 calculate()
简化为如下形式:
def calculate(initial_list):
"""计算列表长度。
Args:
initial_list: 用于计算长度的列表。
Returns:
改增后列表的长度。
"""
length = len(initial_list)
return length
得益于此,我们可以通过闭包来保持一些数据的可变状态,进而减少对 length
进行硬编码依赖的函数数量。
Python中的闭包
在 Python 中,我们可以用 lambda
函数来打造闭包。 lambda
函数可以利用简洁的语法来定义函数。如我们所举例的 calculate()
,在 Python 中,我们可以用 lambda
函数以如下方式实现:
calculate = lambda initial_list: len(initial_list)
而在 Python 中,利用内层 lambda
函数来定义 calculate_and_append()
,则需要如下操作:
calculate = lambda initial_list: lambda x: len(initial_list) + x
由此,我们得以剔除函数式的限定,实现了类似于 calculate_and_append()
这类只专注于参数 x
的函数。
calculate = lambda x: len(initial_list) + x
现在,我们便能够以如下方式返回 calculate()
函数:
calculate = calculate([])
而 calculate()
函数在 Python 中的具体应用如下:
calculate(3) # 4
calculate(1) # 5
calculate(5) # 6
如此,你便得以多次执行 calculate()
函数,不断获取不断扩大的列表长度。
不当闭包使用的负面
当我们讨论完闭包的诸多妙用时,自然也需要了解不当使用闭包可能导致的不良后果。
例如,利用闭包来赋存一些预期用于一次执行,而不是长期保持的栈 式数据,这极可能导致我们耗费宝贵的计算资源来保持这些数据的稳定。而且,赋存这些数据,还可能损害我们控制栈溢出的能力。栈溢出时函数在运行中消耗栈 式数据而堆积的速度远高于释放这些数据的速度。当栈溢出发生时,函数就无法继续执行。闭包若使用不当,很可能导致栈溢出的问题。
由此,我们不应利用闭包来赋存那些预期用于一次执行的栈 式数据。换言之,应确保仅赋存预期长期保留的栈 式数据。这种做法,将极大程度地削减栈溢出的危害,同时节约宝贵的计算资源,进而提升我们对栈溢出的控制能力。
闭包的不当使用,往往会给人代码运行极其缓慢的假象。导致这种情况的罪魁祸首,往往是对于栈溢出的错误把控。栈溢出时函数在运行中消耗栈 式数据