16 KiB
sources, brief
| sources | brief | |||||
|---|---|---|---|---|---|---|
|
闭包是函数携带其所引用外部变量环境并在之后继续使用的机制。 |
闭包
闭包是一种与高阶函数、函数式编程和装饰器实现密切相关的机制:当一个内部函数引用了外部函数作用域中的变量,并且这个内部函数被返回、保存或传递到外部继续使用时,它仍然能够“记住”并访问那些外部变量。
更直观地说:
闭包 = 函数 + 该函数运行所需的外部变量环境
在 summaries/00_Overview 中,闭包作为第 7 章“高级主题”的一部分出现,与“返回函数”一起被列为 Python 进阶特性之一。summaries/03_Returning_functions 进一步说明:闭包的核心价值在于让一个函数即使在原始定义环境结束后,仍能携带必要上下文,在未来正确执行。summaries/04_Function_decorators 则展示了闭包在 函数装饰器 中的典型用途:装饰器通过内部包装函数保存被装饰的原函数,并在调用时添加日志、计时等额外行为。
基本思想
闭包通常涉及三个要素:
- 外部函数:定义局部变量、参数或局部状态。
- 内部函数:在函数体内引用外部函数中的变量。
- 返回或保存内部函数:即使外部函数已经执行结束,内部函数仍然可以访问当时捕获的变量。
示意代码:
def make_adder(x):
def add(y):
return x + y
return add
add10 = make_adder(10)
print(add10(5)) # 15
这里 add 就形成了闭包,因为它捕获了外部函数 make_adder 中的变量 x。即使 make_adder(10) 已经返回,add10 仍然记住了 x = 10。
summaries/03_Returning_functions 中的例子也展示了同样机制:
def add(x, y):
def do_add():
print('Adding', x, y)
return x + y
return do_add
调用 add(3, 4) 时,加法并不会立刻发生,而是返回内部函数 do_add:
a = add(3, 4)
a() # 输出 Adding 3 4,并返回 7
这里 do_add 在 add() 已经执行结束后,仍然能够使用 x = 3 和 y = 4。这正是闭包的本质:函数不仅保存代码,还保存代码所依赖的环境。
与返回函数的关系
闭包经常和“函数作为返回值”一起出现。外部函数返回内部函数时,内部函数并不只是一个可执行对象;如果它引用了外层变量,它还会携带对应的变量绑定。
因此,闭包体现了 Python 中“函数是一等对象”的思想:函数可以被创建、赋值、传递、返回,并保存状态。这也使闭包与 函数式编程 密切相关。
需要注意的是,“返回函数”本身不一定总是闭包。只有当返回的内部函数引用了外部作用域中的变量时,才形成闭包。例如:
def outer():
def inner():
return 'hello'
return inner
这里 inner 没有依赖 outer 的局部变量,因此它只是被返回的函数,不体现典型闭包的状态捕获能力。
局部变量为什么还能存在
普通理解中,函数调用结束后,它的局部变量似乎应该消失。但闭包改变了这个直觉:如果内部函数仍然需要某些外部变量,Python 会保留这些变量,使返回后的函数可以继续访问它们。
因此,下面的问题是理解闭包的关键:
a = add(3, 4)
a()
a() 执行时,3 和 4 从哪里来?答案是:它们来自闭包保存的环境。do_add 携带了对 x 和 y 的引用,使这些值在之后仍可用。
同样,在装饰器中也可以问类似问题:
def logged(func):
def wrapper(*args, **kwargs):
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper
当 logged() 已经返回后,wrapper() 为什么仍然知道要调用哪个 func?答案也是闭包:wrapper 捕获并保存了外层 logged(func) 调用时传入的函数对象。
闭包与装饰器
summaries/04_Function_decorators 说明,装饰器的基本思想是用一个函数包裹另一个函数,为原函数添加额外行为。例如日志装饰器:
def logged(func):
def wrapper(*args, **kwargs):
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper
这里 wrapper 是闭包,因为它引用了外层函数 logged 的参数 func。当执行:
def add(x, y):
return x + y
logged_add = logged(add)
logged_add 实际上指向返回的 wrapper。调用时:
logged_add(3, 4)
wrapper 会先输出日志,再调用它保存的原始函数 add。即使 logged(add) 的调用已经结束,wrapper 仍然保留着对 add 的引用。
这也是 Python装饰器 或 函数装饰器 常见实现方式的核心:
- 外层装饰器函数接收原函数
func。 - 内层包装函数
wrapper添加额外逻辑。 wrapper通过闭包保存func。- 装饰器返回
wrapper,用它替代原函数名。
装饰器语法:
@logged
def add(x, y):
return x + y
等价于:
def add(x, y):
return x + y
add = logged(add)
因此,装饰器并不是脱离闭包的独立魔法。很多装饰器本质上就是“返回包装函数的函数”,而包装函数之所以能继续调用原函数,正是因为闭包保存了原函数对象。
闭包、包装函数与可变参数
装饰器中常见的内部函数也称为 包装函数。包装函数需要尽量像原函数一样工作,因此通常使用 Python函数参数 中的可变参数形式:
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
这里有两层重要机制:
*args和**kwargs让wrapper可以接受任意位置参数和关键字参数,从而适配不同函数签名。func来自外层作用域,由闭包保存,使wrapper知道真正要调用的原函数。
因此,装饰器中的闭包通常同时结合了:
- 函数作为参数传入;
- 内部函数作为返回值返回;
- 内部函数捕获外部变量;
- 可变参数转发调用;
- 用返回的新函数替换旧函数。
这些特性共同体现了 Python 函数对象的灵活性。
计时装饰器中的闭包
summaries/04_Function_decorators 的练习要求实现一个 timethis(func) 装饰器,用来测量函数执行时间:
import time
def timethis(func):
def wrapper(*args, **kwargs):
start = time.time()
r = func(*args, **kwargs)
end = time.time()
print('%s.%s: %f' % (func.__module__, func.__name__, end-start))
return r
return wrapper
这个例子进一步展示了闭包在诊断工具中的作用:
wrapper捕获原函数func。- 调用
wrapper时,会在执行原函数前后记录时间。 func.__module__和func.__name__用于输出被测函数的模块名和函数名。- 原函数的返回值
r被保留并返回,使装饰器尽量不改变原函数的语义。
使用方式:
@timethis
def countdown(n):
while n > 0:
n -= 1
这里 countdown 被重新绑定为 timethis(countdown) 返回的 wrapper。但 wrapper 仍通过闭包记住原始 countdown 函数,因此可以在添加计时逻辑后继续调用它。
这个模式说明,闭包不仅能保存简单数值,也能保存函数对象、配置参数、统计状态等运行上下文。
闭包的常见用途
闭包是 Python 中非常重要但有时较隐蔽的特性。常见用途包括:
- 保存配置或状态:在不使用类的情况下,为函数绑定参数或上下文。
- 创建函数工厂:根据不同输入生成具有不同行为的新函数。
- 回调函数:把带有上下文的函数传给其他代码稍后调用。
- 延迟执行:先构造函数,之后再执行。
- 装饰器实现:许多 Python装饰器 和 函数装饰器 依赖闭包保存被包装函数及附加状态。
- 包装横切逻辑:日志、计时、调试、权限检查等 横切关注点 可以通过闭包集中封装。
- 减少重复代码:用函数生成重复结构,例如属性、检查逻辑或包装函数。
延迟执行
summaries/03_Returning_functions 用 after(seconds, func) 展示了闭包在延迟执行中的作用:
def after(seconds, func):
import time
time.sleep(seconds)
func()
如果直接传入普通函数:
def greeting():
print('Hello Guido')
after(30, greeting)
after 会在等待后调用 greeting。
闭包可以让这个被延迟执行的函数携带额外信息:
def add(x, y):
def do_add():
print(f'Adding {x} + {y} -> {x+y}')
return do_add
after(30, add(2, 3))
这里 add(2, 3) 返回的 do_add 是闭包,它保留了 x = 2 和 y = 3。即使真正执行发生在 30 秒后,它仍然知道要计算什么。
这类模式与 concepts/回调函数 和 延迟求值 密切相关:函数被提前构造,执行被推迟,但上下文不会丢失。
函数工厂
闭包常用于创建“函数工厂”:一个函数根据输入生成另一个函数。
例如:
def power_factory(n):
def power(x):
return x ** n
return power
square = power_factory(2)
cube = power_factory(3)
square 和 cube 都是闭包,它们分别保存了不同的 n。调用时:
square(5) # 25
cube(5) # 125
这里不需要定义两个不同类,也不需要反复传入指数参数;闭包已经把配置保存到了函数内部。
装饰器也可以看作一种特殊的函数工厂:它接收一个函数,生成另一个包装函数。若装饰器再带参数,则通常会出现更多层函数嵌套,每一层都可能通过闭包保存不同的配置或状态。
用闭包减少重复代码
闭包还可以用于“生成代码式结构”,避免大量重复样板代码。summaries/03_Returning_functions 中的核心示例是创建带类型检查的属性。
原始写法中,每个属性都需要重复编写 getter、setter 和类型检查逻辑:
@property
def shares(self):
return self._shares
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected int')
self._shares = value
如果 name、shares、price 都要做类似检查,就会出现大量重复。可以改用闭包生成属性:
def typedproperty(name, expected_type):
private_name = '_' + name
@property
def prop(self):
return getattr(self, private_name)
@prop.setter
def prop(self, value):
if not isinstance(value, expected_type):
raise TypeError(f'Expected {expected_type}')
setattr(self, private_name, value)
return prop
这里 prop 是闭包,因为它捕获了:
nameprivate_nameexpected_type
每次调用 typedproperty(),都会生成一个新的属性对象,并且该属性对象记住自己的私有字段名和期望类型。
于是可以这样定义类:
class Stock:
name = typedproperty('name', str)
shares = typedproperty('shares', int)
price = typedproperty('price', float)
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
当执行:
s = Stock('IBM', 50, 91.1)
s.shares = '100'
赋值会触发 setter,并因类型不匹配抛出 TypeError。
这个例子说明,闭包可以与 Python属性、Python描述符 和类定义机制配合,用来构造可复用的行为模板。
装饰器中的日志和计时示例也体现了同一个原则:如果某段辅助逻辑反复出现在很多函数中,可以把它移到闭包和包装函数中统一管理,从而减少重复并提升可维护性。
与 lambda 的配合
闭包也可以和 lambda表达式 配合,用来进一步简化接口。
例如在 typedproperty 的基础上定义:
String = lambda name: typedproperty(name, str)
Integer = lambda name: typedproperty(name, int)
Float = lambda name: typedproperty(name, float)
这样类定义可以写得更简洁:
class Stock:
name = String('name')
shares = Integer('shares')
price = Float('price')
这里 lambda 本身也是函数对象,也可以捕获或绑定上下文。它常被用于创建短小的函数工厂包装层,从而减少重复参数。
与类和对象状态的关系
闭包和类都可以保存状态,但表达方式不同:
- 类通常把状态保存在实例属性中,通过方法访问。
- 闭包把状态保存在函数的外部变量环境中,通过返回的内部函数访问。
例如:
def make_counter():
count = 0
def counter():
return count
return counter
这类结构类似一个轻量对象:函数 counter 携带了状态 count。当状态和行为较简单时,闭包可以替代小型类;当状态复杂、行为较多或需要继承时,类通常更清晰。
装饰器中的 wrapper 也类似一个轻量对象:它把“原函数”作为状态保存起来,并在每次调用时围绕这个状态执行额外逻辑。不过,如果装饰逻辑需要维护复杂状态,类装饰器或描述符有时会更适合。
与相关概念的联系
- 函数式编程:闭包是函数式编程中的重要机制,支持函数组合、函数工厂和高阶函数。
- Python装饰器:装饰器通常通过闭包保存原函数,并返回包装后的新函数。
- 函数装饰器:函数装饰器中的
wrapper往往就是捕获func的闭包。 - 包装函数:包装函数通过闭包记住被包装对象,并在调用前后添加额外行为。
- 横切关注点:日志、计时等横切逻辑可由闭包和装饰器统一封装。
- lambda表达式:
lambda也可以捕获外部变量,因此在某些情况下也会形成闭包。 - Python函数参数:闭包有时可替代反复传参,通过捕获外部变量保存上下文;装饰器中也常用
*args、**kwargs转发参数。 - concepts/回调函数:闭包可以把回调逻辑和回调所需上下文打包在一起。
- 延迟求值:闭包允许函数稍后执行,同时保留构造时的变量环境。
- Python属性:闭包可以生成
property对象,减少 getter/setter 的重复代码。 - Python函数对象:闭包依赖函数可被传递、返回和保存的特性,也可访问函数的
__name__、__module__等属性。
学习提示
理解闭包时,重点应放在以下问题上:
- 内部函数引用了哪些外部变量?
- 外部函数返回后,这些变量为什么仍然可用?
- 闭包是在保存代码,还是在保存代码加环境?
- 返回函数是否真的引用了外部变量,还是只是普通地返回了一个函数?
- 在装饰器中,包装函数如何记住被装饰的原函数?
*args和**kwargs如何帮助包装函数透明地转发调用?- 这个场景更适合闭包,还是更适合类?
- 闭包是否能减少重复代码,或让延迟执行更方便?
- 闭包与装饰器、回调函数、属性工厂之间有什么关系?
summaries/00_Overview 强调,闭包属于 Python 日常编程中可能遇到的进阶主题。summaries/03_Returning_functions 通过返回函数、延迟执行和类型化属性示例说明:闭包不仅是语法技巧,更是一种保存上下文、生成函数和消除重复代码的重要工具。summaries/04_Function_decorators 进一步展示了闭包在装饰器中的实际价值:它让包装函数能够保存原函数,并在不修改原函数主体的情况下添加日志、计时等可复用行为。
See also: summaries/02_Anonymous_function, summaries/03_Returning_functions, summaries/04_Function_decorators