Python 中的闭包(Closure)
在 Python 中,闭包(Closure) 是一个非常重要且强大的概念。简单来说,闭包让一个函数能够“记住”并访问其定义时所在的外部作用域中的变量,即使那个外部作用域的函数已经执行结束了。
以下是关于 Python 闭包的详细解析:
1. 什么是闭包?
要形成一个闭包,必须同时满足以下 三个条件:
- 嵌套函数:必须有一个嵌套在其他函数内部的函数(即:外部函数里面定义了内部函数)。
- 引用外部变量:内部函数必须引用外部函数中定义的变量(非全局变量)。
- 返回内部函数:外部函数必须返回内部函数的引用。
一句话总结:闭包 = 内部函数 + 定义时的环境(外部变量)。
2. 基本示例
让我们看一个最经典的例子:
def make_multiplier(n):
# 外部函数
def multiplier(x):
# 内部函数
# 这里引用了外部函数的变量 n
return x * n
# 返回内部函数
return multiplier
# 1. 创建闭包
times3 = make_multiplier(3) # n = 3
times5 = make_multiplier(5) # n = 5
# 2. 调用闭包
print(times3(10)) # 输出: 30 (10 * 3)
print(times5(10)) # 输出: 50 (10 * 5)
发生了什么?
- 当我们调用
make_multiplier(3)时,外部函数执行完毕并返回了multiplier函数。 - 通常情况下,函数执行完后,其局部变量(如
n)会被销毁。 - 但在闭包中,
times3这个函数对象“携带”了n=3这个环境信息。即使make_multiplier早就结束了,times3依然记得n是 3。
3. 查看闭包的内部 (__closure__)
Python 将闭包引用的外部变量存储在函数对象的 __closure__ 属性中。
print(times3.__closure__)
# 输出: (<cell at 0x...: int object at 0x...>,)
print(times3.__closure__[0].cell_contents)
# 输出: 3
这证明了变量 n 被保存在了一个特殊的“单元(cell)”中,跟随函数对象一起存在。
4. 修改闭包变量 (nonlocal 关键字)
默认情况下,内部函数只能读取外部变量。如果你尝试在内部函数中修改外部变量(例如重新赋值),Python 会认为你是在定义一个新的局部变量,从而报错或产生意外结果。
在 Python 3 中,使用 nonlocal 关键字可以解决这个问题。
示例:计数器
def make_counter():
count = 0 # 外部变量
def inner():
nonlocal count # 声明我们要修改外部的 count
count += 1
return count
return inner
counter = make_counter()
print(counter()) # 输出: 1
print(counter()) # 输出: 2
print(counter()) # 输出: 3
如果没有 nonlocal count,count += 1 会抛出 UnboundLocalError。
5. 闭包的实际应用场景
你可能在想:“我为什么要用闭包?直接用类(Class)不行吗?”
闭包通常用于以下场景,比类更轻量、更优雅:
A. 数据隐藏与封装
闭包可以避免使用全局变量,将数据隐藏在函数内部,只有内部函数能访问。
B. 装饰器 (Decorators)
这是 Python 中闭包最广泛的应用。装饰器本质上就是一个闭包:它接受一个函数,返回一个包装后的新函数。
def my_decorator(func):
def wrapper():
print("在函数执行前做点什么...")
func()
print("在函数执行后做点什么...")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
C. 延迟计算 (Lazy Evaluation)
你可以预先配置函数的某些参数,等到真正需要计算时再调用(类似于偏函数 functools.partial)。
6. 常见的“坑”:循环中的闭包
这是一个面试中经常出现的问题:延迟绑定(Late Binding)。
错误示例:
functions = []
for i in range(3):
def inner():
return i # 引用了外部变量 i
functions.append(inner)
# 你期望输出 0, 1, 2
# 但实际输出是:
print(functions[0]()) # 2
print(functions[1]()) # 2
print(functions[2]()) # 2
原因:闭包引用的是变量 i 的内存地址,而不是 i 在定义时的值。当循环结束时,i 的值变成了 2。所有的闭包都指向同一个 i,所以它们都返回 2。
解决方案:使用默认参数将当前的值绑定到局部变量。
functions = []
for i in range(3):
def inner(x=i): # x 在定义时就被赋值为当前的 i
return x
functions.append(inner)
print(functions[0]()) # 0
print(functions[1]()) # 1
print(functions[2]()) # 2
总结
- 定义:闭包是引用了自由变量的函数。
- 核心:即使外部作用域失效,闭包依然能记住外部变量。
- 修改:修改外部变量需要使用
nonlocal。 - 用途:装饰器、数据隐藏、函数工厂。