完整讲解 · 4 段教学·配 3 道练习题·预计 25 分钟
本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。
这一节我们通过解决一个实际需求来认识闭包。
我们需要一直记录自己的学习时间,以分钟为单位。比如:
212(累加)最直觉的写法是用一个全局变量记录时间,然后用一个函数往里加:
time = 0
def insert_time(min):
time = time + min
return time
print(insert_time(2))
print(insert_time(10))认真想一下,这段代码会有什么问题呢?
其实,这段代码在 Python 里会报错:
UnboundLocalError: local variable 'time' referenced before assignment
为什么?因为在 Python 中,如果一个函数里给一个名字赋值了,这个名字就被认为是局部变量——哪怕外面有同名的全局变量。函数内 time = time + min 这一行,time 已经被当成局部变量了,但右边又先去取它的值,所以报「先用后定义」。
如果确实要在函数中引用并修改全局变量,可以用 global 关键字:
time = 0
def insert_time(min):
global time
time = time + min
return time
print(insert_time(2))
print(insert_time(10))输出:
2
12
可是啊,这里使用了全局变量——开发中能避免就尽量避免。
为什么?
那有没有更优雅的办法呢?这就是闭包登场的时候了。
我们直接看代码:
def study_time(time):
def insert_time(min):
nonlocal time
time = time + min
return time
return insert_time
f = study_time(0)
print(f(2))
print(f(10))输出:
2
12
这就是闭包——「函数里嵌套函数,内层函数返回出去带着外层变量」。
当一个内部函数被当作对象返回时,它夹带了外部函数的局部变量,就形成了一个闭包。
换句话说:内部函数的局部作用域中可以访问外部函数局部作用域中的变量,这种行为就叫闭包(Closure)。
回头看上面的代码:
def study_time(time): # 外层函数:接收一个 time
def insert_time(min): # 内层函数:实现累加逻辑
nonlocal time
time = time + min
return time
return insert_time # 外层返回内层函数对象(在 Python 中,函数也是对象)
f = study_time(0) # 调用 study_time,f 现在等于 insert_time 函数
print(f(2)) # 相当于调用 insert_time(2)
print(f(10)) # 相当于调用 insert_time(10)f 不是数字,不是字符串——它是一个带着 time 这个变量的函数。每次调用 f 时,那个 time 还在它身边,所以累加可以一直延续。
听起来挺玄,下一节我们会重点讲让闭包修改外层变量的关键字 nonlocal,并做几道练习。
上一节代码里有一行 nonlocal time,它是干嘛的?
回顾一下作用域规则:
globalnonlocal闭包的场景就是「内层函数想改外层函数的变量」,刚好对应 nonlocal。
def make_counter():
count = 0
def counter():
count = count + 1 # 没写 nonlocal —— 报 UnboundLocalError
return count
return counter直接调 make_counter()() 会报错,原因和第一节里全局变量那个例子一样。
加上 nonlocal:
def make_counter():
count = 0
def counter():
nonlocal count
count = count + 1
return count
return counter
c = make_counter()
print(c())
print(c())
print(c())输出:
1
2
3
每次调用 c(),闭包里的 count 都会增加并被记住——一个独立的计数器就这么做出来了。
如果只是读取外层变量、不修改它,是不用 nonlocal 的:
def make_greeter(name):
def greet():
print(f"你好,{name}") # 只读,不需要 nonlocal
return greet
g = make_greeter("两点水")
g()输出:
你好,两点水
一句话总结:要在内层函数里给外层变量重新赋值,就要
nonlocal;只读不用。
闭包可以根据参数生成定制化的函数——典型用法是「加法器工厂」:
def make_adder(n):
def adder(x):
return x + n
return adder
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(3)) # 8
print(add10(3)) # 13make_adder(5) 和 make_adder(10) 返回了两个不同的函数,但它们各自带着自己的 n,互不干扰。这就是闭包的「绑定」效果。
刚才学的 make_counter 就是这一类——把状态藏在闭包里,外面看不见、也改不了。
def make_counter():
count = 0
def counter():
nonlocal count
count = count + 1
return count
return counter
c1 = make_counter()
c2 = make_counter()
print(c1()) # 1
print(c1()) # 2
print(c2()) # 1,c1 和 c2 互不影响每个 make_counter() 调用都创建一个独立的闭包,各自维护自己的 count。
所有函数都有一个 __closure__ 属性。普通函数它是 None;闭包它是一个由 cell 组成的元组,cell 的 cell_contents 就是闭包绑定的变量。
def make_counter():
count = 0
def counter():
nonlocal count
count = count + 1
return count
return counter
c = make_counter()
print(c.__closure__ is not None) # True
c()
c()
print(c.__closure__[0].cell_contents) # 2输出:
True
2
闭包的过程其实好比类(父函数)生成实例(闭包)。父函数只在调用时执行,执行完毕后其环境就会释放——但闭包还在,因为内层函数把它「带走」了。
对那些需要重用、但又不足以定义为类的小功能,用闭包比写类更轻量、更灵活。
下一章我们学装饰器——它就是闭包最经典的应用。
读完了?动手练一遍才算真会。
去闯关模式练习 →