1. 引言
在 Python 中,函数是一等公民(first-class citizen),可以像普通变量一样被赋值、传递和返回。正是这一特性,让「闭包」成为 Python 中一个既基础又强大的概念。
很多初学者在第一次看到「闭包」这个词时会觉得抽象,但一旦理解了它的本质------一个携带了外部作用域变量的函数------就会发现它其实非常自然。闭包广泛应用于装饰器、回调函数、函数式编程、状态保持等场景,是进阶 Python 开发者必须掌握的知识点。
本文将从作用域讲起,逐步揭开闭包的面纱,并通过大量可运行的代码示例,带你彻底搞懂 Python 闭包。
2. 前置知识:Python 的作用域
要理解闭包,首先要理解 Python 的变量作用域。Python 遵循 LEGB 规则,即变量查找顺序为:
- L(Local):函数内部作用域
- E(Enclosing):外层嵌套函数作用域
- G(Global):模块全局作用域
- B(Built-in):内置作用域
python
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # 输出 local
inner()
outer()
当 inner 中访问 x 时,会优先使用自己作用域内的 x。如果 inner 中没有定义 x,则会向外层 outer 查找,再找不到就去全局和内置作用域查找。
3. 什么是闭包
闭包(Closure) 是指一个函数与其引用的外部(非全局)变量的组合。通俗地说,当一个内部函数引用了外部函数的变量,并且外部函数把内部函数作为返回值返回时,就形成了闭包。
python
def outer(msg):
def inner():
print(msg)
return inner
greet = outer("Hello, Closure!")
greet() # 输出 Hello, Closure!
在上面的例子中:
inner是内部函数;inner引用了外部函数outer的参数msg;outer返回了inner;- 即使
outer已经执行完毕,inner依然能访问msg。
这就是闭包的核心:外部函数执行结束后,其局部变量并不会被销毁,而是被内部函数「记住」了。
3.1 闭包成立的三个条件
一个闭包通常需要同时满足以下三个条件:
- 存在函数嵌套(内部函数定义在外部函数中);
- 内部函数引用了外部函数的变量(自由变量);
- 外部函数返回了内部函数。
4. 闭包是如何工作的
理解闭包的关键在于 Python 的函数对象 和**__closure__ 属性**。
python
def outer(msg):
def inner():
print(msg)
return inner
greet = outer("Hello")
print(greet.__closure__) # (<cell at 0x...: str object at 0x...>,)
print(greet.__closure__[0].cell_contents) # Hello
每个函数对象都有一个 __closure__ 属性,它是一个元组,保存了闭包所引用的所有自由变量(以 cell 对象的形式)。通过 cell_contents 可以取出实际的值。
此外,还可以通过 __code__.co_freevars 查看自由变量的名字:
python
print(greet.__code__.co_freevars) # ('msg',)
这说明 msg 被识别为自由变量,并被保存在闭包中。
5. 闭包的常见应用场景
5.1 状态保持(计数器)
闭包可以在不使用全局变量或类的情况下保存状态:
python
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
这里必须使用 nonlocal 关键字,否则 count += 1 会被当作在 counter 内部定义了一个新的局部变量,从而抛出 UnboundLocalError。
5.2 装饰器
装饰器是闭包最经典的应用。一个简单的计时装饰器:
python
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 执行耗时: {end - start:.4f}s")
return result
return wrapper
@timer
def slow_task():
time.sleep(1)
return "done"
slow_task()
wrapper 就是一个闭包,它引用了外部传入的 func。
5.3 函数工厂
闭包可以根据参数动态生成不同行为的函数:
python
def power(exp):
def calc(base):
return base ** exp
return calc
square = power(2)
cube = power(3)
print(square(5)) # 25
print(cube(5)) # 125
5.4 回调函数与延迟执行
python
def make_callback(name):
def callback():
print(f"回调触发: {name}")
return callback
callbacks = [make_callback(f"task-{i}") for i in range(3)]
for cb in callbacks:
cb()
6. 闭包的常见陷阱
6.1 循环中的闭包
这是最经典的坑:
python
funcs = []
for i in range(3):
funcs.append(lambda: i)
for f in funcs:
print(f()) # 输出 2 2 2,而不是 0 1 2
原因在于闭包捕获的是变量本身 ,而不是变量在某一时刻的值。循环结束后 i 的值为 2,所以所有 lambda 都输出 2。
解决办法是使用默认参数绑定当前值:
python
funcs = []
for i in range(3):
funcs.append(lambda i=i: i)
for f in funcs:
print(f()) # 输出 0 1 2
6.2 修改外部变量需要 nonlocal
python
def outer():
x = 0
def inner():
x += 1 # UnboundLocalError
inner()
outer()
正确写法是声明 nonlocal x。如果外部变量在全局作用域,则需要使用 global。
6.3 闭包与内存
闭包会持有外部变量的引用,如果这些变量占用大量内存,可能导致内存无法及时释放。在不需要时,应避免长期持有闭包对象。
7. 闭包 vs 类
闭包和类都可以用来保存状态,如何选择?
| 对比项 | 闭包 | 类 |
|---|---|---|
| 代码量 | 更简洁 | 相对冗长 |
| 状态数量 | 适合少量状态 | 适合复杂状态 |
| 可读性 | 简单场景更直观 | 复杂场景更清晰 |
| 扩展性 | 较弱 | 强(继承、多态) |
| 调试 | 相对困难 | 更友好 |
一般建议:状态简单、逻辑轻量时用闭包;状态复杂、需要扩展时用类。
8. 总结
闭包是 Python 中一个优雅而实用的特性,它的本质是函数与其引用的外部变量的组合。掌握闭包,需要理解以下几点:
- Python 的 LEGB 作用域规则;
- 内部函数引用外部变量,并由外部函数返回;
- 通过
__closure__和co_freevars可以窥探闭包的内部结构; - 闭包常用于状态保持、装饰器、函数工厂和回调;
- 注意循环变量捕获、
nonlocal声明和内存持有等陷阱。
理解闭包不仅能帮助你写出更简洁的代码,更是深入理解装饰器、生成器、函数式编程等高级特性的必经之路。建议读者动手敲一遍文中的示例,加深理解。