完整讲解 · 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,并做几道练习。
请定义一个函数 make_adder(n):
nadder(x),作用是返回 x + n然后:
make_adder(10) 生成 add10print(add10(5)) —— 应该输出 15make_adder(100) 生成 add100print(add100(5)) —— 应该输出 105输出应该是:
15
105
内层函数只读 n,不修改它,所以不需要 nonlocal
make_adder(10) 和 make_adder(100) 是两个不同的闭包,各自带着不同的 n
完整代码:
def make_adder(n):
def adder(x):
return x + n
return adder
add10 = make_adder(10)
print(add10(5))
add100 = make_adder(100)
print(add100(5))
上一节代码里有一行 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;只读不用。
请定义一个函数 make_counter():
count = 0counter(),每次调用 counter() 时让 count 加 1 并返回新值(记得用 nonlocal)然后:
c = make_counter()c(),每次都 print 它的返回值输出应该是:
1
2
3
外层函数定义 count = 0,内层函数用 nonlocal count 才能改它
外层函数 return 内层函数对象(注意:return counter,不是 return counter())
完整代码:
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())
闭包可以根据参数生成定制化的函数——典型用法是「加法器工厂」:
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
闭包的过程其实好比类(父函数)生成实例(闭包)。父函数只在调用时执行,执行完毕后其环境就会释放——但闭包还在,因为内层函数把它「带走」了。
对那些需要重用、但又不足以定义为类的小功能,用闭包比写类更轻量、更灵活。
下一章我们学装饰器——它就是闭包最经典的应用。
回到本章开头的需求——用闭包累加学习时间。
请定义函数 study_time(time):
timeinsert_time(min),每次调用时把 min 累加到 time 上并返回当前总时间(记得用 nonlocal)然后:
f = study_time(0)print(f(2)) —— 学了 2 分钟print(f(10)) —— 又学了 10 分钟print(f(5)) —— 又学了 5 分钟输出应该是:
2
12
17
外层 study_time 接收 time 作为参数;内层 insert_time 通过 nonlocal time 修改它(参数名 min 沿用原教程写法,它会临时遮住内置函数 min()——真实项目里建议用 minutes 这类不冲突的名字)
外层函数 return 内层函数对象 insert_time(不要带括号)
完整代码:
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))
print(f(5))
读完了?动手练一遍才算真会。
去闯关模式练习 →