闭包
约 1376 字大约 5 分钟
2025-04-04
闭包 = 内层函数 + 它“记住”的外层变量。理解它的关键是“外层函数已经返回,内层却还在用外层的变量”。这是装饰器、回调、状态封装等大量进阶语法的底层基础。
- 闭包的两个必要条件:用了外层非全局变量、把内层函数返回出去
- 自由变量与单元格 (cell) 对象
- 闭包 vs 全局变量:封装而非污染
- __closure__ / __code__.co_freevars 自查工具
- 1写一个 make_counter() 返回内层函数,每次调用 counter() 就 +1,体会“外层变量在内层被记住”。
- 2对闭包打印 fn.__closure__ —— 看到 cell 对象就是它“偷藏”的外层变量。
- 3内层想修改外层变量(不是只读)必须 nonlocal x —— 漏写会得到 UnboundLocalError。
- 4把全局列表 lst 改成闭包内的局部 lst,体会“数据不再可能被外部误改”这层保护。
- 5记忆:装饰器之所以是装饰器,就是因为底层是闭包 —— 这一节没吃透下一节会更费劲。
在编程时,我们会处理到很多数据。但是对于一些数据,我们只想由某个函数自己使用,不想被其他逻辑修改。我们可以使用 闭包 来防止不经意间的数据修改。
闭包的作用主要有两个:
- 保护数据安全
- 保护数据干净性
满足下面两个条件的函数就是一个实用的闭包:
- 在嵌套函数内,使用非全局变量(且不使用本层变量)
- 将嵌套函数本身返回
我们可以用.__closure__方法来查看一个函数是否是一个闭包。当返回值为 None 时,说明该函数不是闭包,当返回值中有变量的内存地址时,说明该函数是一个闭包。
例如,下面的函数就是一个闭包:
python playground
def func():
a = 10 # 自由变量
def foo():
print(a)
return foo
f = func()
print(f.__closure__) # (<cell at 0x000001F7D6277648: int object at 0x00000000669D8190>,)方法中的 closure 就是 闭包 的意思。返回的 cell 是一个 单元格 对象,用来储存闭包中需要使用到的变量。
当函数执行完成后(也就是 fun() 这个步骤),函数体会被销毁。但是闭包 foo 被返回给了 f,还会被用到,所以照常存在。而闭包中需要使用的变量 a,此时已经不在函数 func 中,因为 func 本身已经不复存在。变量 a 在函数销毁后,升级为 自由变量。
闭包是怎么做到保护数据的作用的呢?
比如我们现在有这样一个需求:使用一个函数,每天输入当天营业额,打印出这几天的平均营业额。
如果我们把营业额的数据储存在列表中,放到全局,可以这样实现功能:
python playground
lst = []
def ave_turnover(turnover_today):
lst.append(turnover_today)
ave = sum(lst) / len(lst)
print(ave)
ave_turnover(12000) # 12000.0
ave_turnover(15000) # 13500.0
ave_turnover(14000) # 13666.666666666666
ave_turnover(12000) # 13250.0
ave_turnover(25000) # 15600.0
ave_turnover(21000) # 16500.0
ave_turnover(9000) # 15428.57142857143如果有一天,我们不小心在全局对 lst 列表做了修改:
python playground
lst = []
def ave_turnover(turnover_today):
lst.append(turnover_today)
ave = sum(lst) / len(lst)
print(ave)
ave_turnover(12000) # 12000.0
ave_turnover(15000) # 13500.0
lst[1] = 10
ave_turnover(14000) # 8670.0
ave_turnover(12000) # 9502.5
ave_turnover(25000) # 12602.0
ave_turnover(21000) # 14001.666666666666
ave_turnover(9000) # 13287.142857142857这就导致了数据的改变,使得第三天之后的数据全都不准确。
如果我们把重要的数据 lst 封装到闭包中保护起来,就不会有这种烦恼了:
python playground
def ave_turnover():
lst = []
def inner(turnover_today):
lst.append(turnover_today)
ave = sum(lst) / len(lst)
print(ave)
return inner
f = ave_turnover()
f(12000) # 12000.0
f(15000) # 13500.0
lst = [10]
f(14000) # 13666.666666666666
f(12000) # 13250.0
f(25000) # 15600.0
f(21000) # 16500.0
f(9000) # 15428.57142857143对于闭包,除了 .__closure__ 方法之外,还有可以用来查看自由变量的 .__code__.co_freevars 方法,和可以用来查看局部变量的 .__code__.co_varnames 方法:
python playground
def ave_turnover():
lst = []
def inner(turnover_today):
lst.append(turnover_today)
ave = sum(lst) / len(lst)
print(ave)
return inner
f = ave_turnover()
print(f.__closure__) # 判定是否是一个闭包 # (<cell at 0x0000027A31A27648: list object at 0x0000027A31AD8C08>,)
print(f.__code__.co_freevars) # 查看自由变量 # ('lst',)
print(f.__code__.co_varnames) # 查看局部变量 # ('turnover_today', 'ave')没有将嵌套的函数返回的函数也可以是一个闭包,但是这个闭包没有办法被使用:
python playground
def func():
a = 10
def foo():
print(a)
print(foo.__closure__)
func() # (<cell at 0x000001BE85EA7648: int object at 0x00000000669D8190>,)闭包也可以通过外层函数加两个括号的方法来调用:
python playground
def func():
a = 10
def foo():
print(a)
return foo
func()()外层函数的参数也是局部变量,所以内层函数使用外层参数的形式也可以构成闭包:
python playground
def wrapper(a, b):
def inner():
print(a)
print(b)
return inner
a = 11
b = 12
ret = wrapper(a, b)
print(ret.__closure__) # (<cell at 0x0000021A67AFB648: int object at 0x00000000702D81B0>, <cell at 0x0000021A67AFB9A8: int object at 0x00000000702D81D0>)
print(ret.__code__.co_freevars) # ('a', 'b')
print(ret.__code__.co_varnames) # ()
ret()闭包的应用场景主要有两个:
- 装饰器
- 防止数据被误改动
- 闭包 = 内层函数 + “记住”的外层变量;判定看 __closure__ 是不是 None。
- 外层函数返回后,自由变量被 cell 对象捕获,不会随作用域销毁。
- 内层只读外层变量没问题;要修改必须 nonlocal,否则被当成新建局部变量。
- 闭包是装饰器的底层结构,也是“轻量级类”——只想藏几个状态时比类更简洁。
版权所有
版权归属:Shuo Liu
