《Python 高阶教程》010|闭包真正的用法:不是概念,而是解决问题的工具
很多人学闭包,卡住的不是语法,而是不知道它到底有啥用
一提到闭包,不少人脑子里会立刻冒出几个词:
内层函数
外层变量
记住状态
nonlocal
这些词单独看都不算难,可一组合起来,很多人就开始迷糊。更常见的问题是,就算勉强背下了定义,写代码时还是不知道什么时候该用,最后只能把闭包当成一个面试概念。
其实闭包没有那么玄。
你可以把它理解成一种很实用的能力:
函数执行结束后,里面的某些数据本来应该消失,但因为被返回出去的内层函数引用住了,这些数据还能继续活着,并在后续调用中反复使用。
说得再直白一点:
闭包就是让函数既能做事,又能偷偷带着一份自己的小状态一起走。
这件事一旦理解透,你会发现闭包并不是为了显得高级,而是特别适合解决一类问题:
我想保存状态,但又不想把这份状态扔到全局变量里
我想给函数提前绑定一些配置
我想动态生成一组功能相似、规则不同的函数
我想做一个轻量、私有、局部的小对象效果
所以这一章的重点,不是背定义,而是把闭包真正讲到能用。
先看最基础的版本:为什么外层函数结束了,里面的变量还没死
先看一个例子:
def outer():
x = 10
def inner():
return x
return inner
func = outer()
print(func())
输出是:
10
很多人第一次看到这里,会觉得有点奇怪。
outer 明明已经执行完了。
按前面讲作用域时的理解,x 是 outer 的局部变量,按理说应该随着函数结束一起消失。
可为什么 inner 还能拿到它。
原因就在这里:
inner 这个函数引用了 x
而 inner 又被 outer 返回了出去
所以 x 不能立刻销毁,它得继续活着,供 inner 后续使用
这就是闭包最核心的现象。
不是变量“神奇复活”了。
而是这个变量本来就因为被内层函数引用着,所以没有死。
所以以后你看见闭包,脑子里先建立一个画面:
外层函数执行完了
但它里面有些数据被内层函数背走了
只要这个内层函数还活着,这些数据就继续活着
这个画面比死记硬背定义有用得多。
先别急着背术语,先认识闭包到底由什么组成
一个典型闭包,通常包含三样东西。
第一,外层函数
第二,内层函数
第三,内层函数引用了外层函数的局部变量
比如:
def outer():
message = "你好"
def inner():
return message
return inner
这里:
outer 是外层函数
inner 是内层函数
message 是外层局部变量,同时被 inner 引用了
当 outer 返回 inner 时,就形成了一个典型闭包。
注意一点,不是只要有嵌套函数就叫闭包。
比如下面这个:
def outer():
def inner():
return "hello"
return inner
这里虽然也是嵌套函数,但 inner 并没有用到 outer 的局部变量。
严格来说,它就不算最典型意义上的闭包。
所以闭包真正关键的不是嵌套,而是引用外层状态。
为什么闭包和普通返回函数不一样
看两个例子对比一下。
第一个:
def outer():
def inner():
return "hello"
return inner
第二个:
def outer():
message = "hello"
def inner():
return message
return inner
这两个都返回了一个函数。
但第二个比第一个多了一层东西:它把外层的状态一起带出去了。
这就是闭包和普通“返回函数”最大的区别。
前者只是把一个函数返回出去。
后者不只返回函数,还返回了一份和这个函数绑定在一起的上下文状态。
说白了:
普通返回函数,是送你一把工具。
闭包,是送你一把工具,还顺手给你装好一套专属配置。
闭包最常见的实际用途之一:给函数提前绑定参数
看一个例子。假设你想做一批加法函数:
一个固定加 5
一个固定加 10
一个固定加 100
最普通的写法可能是:
def add_5(x):
return x + 5
def add_10(x):
return x + 10
def add_100(x):
return x + 100
这也能写,但明显很笨。
因为它们的区别只在那个固定值上。
更自然的方式,是用闭包生成:
def make_adder(n):
def add(x):
return x + n
return add
add_5 = make_adder(5)
add_10 = make_adder(10)
add_100 = make_adder(100)
print(add_5(3))
print(add_10(3))
print(add_100(3))
输出是:
8
13
103
这里的 n 就是被闭包记住的外层状态。
你可以把 make_adder 理解成一个函数工厂。
它根据不同的配置 n,生产出不同行为的函数。
这就是闭包最常见、也最好理解的用途之一:
把一部分参数先固定住,生成更具体的函数。
这种提前绑定配置的能力,为什么很有价值
因为它能把通用逻辑和个性化参数拆开。
看上面的例子,本质上所有函数都在做同一件事:
接收一个 x
返回 x 加上某个固定值
通用逻辑只有一份。
变化的只有那个固定值。
如果不用闭包,你就得不断重复写类似函数。
而用了闭包,变化部分变成配置,通用部分保留在工厂函数里。
真实项目里,这个思路特别常见。
比如:
生成不同折扣规则
生成不同校验规则
生成不同日志前缀函数
生成不同权限判断函数
生成不同格式化函数
这些场景本质都一样:
同一类行为,只是配置不同。
闭包天生适合干这个。
再看一个更实用的例子:生成不同长度校验器
假设你要校验用户名和密码长度。
用户名长度 3 到 10
密码长度 6 到 20
如果硬写,可能是这样:
def check_username(text):
return 3 <= len(text) <= 10
def check_password(text):
return 6 <= len(text) <= 20
这也没问题。
但一旦规则变多,就开始重复。
用闭包改写:
def make_length_checker(min_len, max_len):
def check(text):
return min_len <= len(text) <= max_len
return check
check_username = make_length_checker(3, 10)
check_password = make_length_checker(6, 20)
print(check_username("tom"))
print(check_password("123456"))
print(check_password("123"))
这里的 min_len 和 max_len 会被内层函数记住。
以后每次调用 check_username 或 check_password,都不需要再把长度范围传一遍。
这就是闭包非常实用的一点:
它能把规则配置和执行动作绑定在一起。
闭包第二个非常实用的用途:保存状态
这是闭包最容易让人眼前一亮的地方。
先看一个简单需求:做一个计数器。
每调用一次,就让计数加一。
很多人第一反应可能是写全局变量:
count = 0
def counter():
global count
count += 1
return count
这能跑,但不够好。
问题在于:
这个状态暴露在全局
任何地方都能改
多个计数器之间也很难互不干扰
闭包版本更自然:
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c1 = make_counter()
c2 = make_counter()
print(c1())
print(c1())
print(c1())
print(c2())
print(c2())
输出大概是:
1
2
3
1
2
这里特别值得体会。
c1 和 c2 都是由同一个工厂函数生成的。
但它们各自有独立的 count。
互不影响。
这就是闭包保存状态的威力:
状态不是全局共享的
而是跟着具体函数实例走的
你会发现,这已经有一点“小对象”的味道了。
为什么这里必须用 nonlocal
因为 count 是外层函数里的局部变量。
而内层函数里要修改它,不只是读取它。
按照前面作用域的规则,只要在内层函数里给 count 赋值,Python 默认就会把它当成当前局部变量。
所以如果不写 nonlocal,就会出错。
比如错误写法:
def make_counter():
count = 0
def counter():
count += 1
return count
return counter
这里会报错。
因为 count += 1 让 Python 认为 counter 里有一个局部 count,但它又在赋值前被读取了。
所以在闭包里,只要你想修改外层变量,通常就要配合 nonlocal。
一句话记住:
只读外层变量,不一定要 nonlocal
要改外层变量,通常就需要 nonlocal
闭包和类,其实能解决一部分相似问题
看了上面的计数器,你可能会想到类:
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
c = Counter()
print(c())
print(c())
这当然也能做,而且还很清晰。
这说明一件事:
闭包和类,并不是非此即彼。
它们都可以保存状态。
区别通常在于:
如果逻辑很轻,状态也少,只是想快速生成一个带状态的小函数,闭包会很方便。
如果状态多、行为多、结构复杂,那类通常更清晰。
所以闭包不是用来取代类的。
它更像一个轻量工具,适合处理“小而精”的状态封装。
什么时候用闭包,比用类更顺手
通常是下面这些场景。
第一,状态非常少。
可能就一两个变量。
第二,只需要暴露一个核心行为。
不需要一堆方法。
第三,想把状态藏起来,不暴露太多接口。
只让外部通过函数使用。
第四,就是想快速生成一组行为类似但配置不同的函数。
比如计数器、简单规则函数、回调处理器、日志函数、格式化函数,这些都很适合闭包。
如果你都要写 init、好几个方法、还有很多属性了,那通常类会更合适。
闭包第三个常见用途:做轻量级工厂函数
前面已经看过加法器、长度校验器。
再看一个更贴近业务的例子。
假设你要生成不同折扣规则:
满减不谈,只看简单折扣
普通会员 95 折
高级会员 9 折
VIP 85 折
可以写成:
def make_discount(rate):
def apply(price):
return price * rate
return apply
normal_discount = make_discount(0.95)
premium_discount = make_discount(0.9)
vip_discount = make_discount(0.85)
print(normal_discount(100))
print(premium_discount(100))
print(vip_discount(100))
这里 apply 就是一个被配置好的专属函数。
不同 rate 生成不同版本。
这种写法非常适合“规则工厂”场景。
以后新增折扣规则,不用重写逻辑,只需要传不同参数。
闭包第四个常见用途:做回调前的预配置
比如你有一个统一日志输出函数,但不同模块想带不同前缀。
普通写法可能是每次都传 prefix:
def log(prefix, message):
print(f"[{prefix}] {message}")
log("订单", "创建成功")
log("支付", "扣款成功")
这当然能用。
但如果某个模块里要连续打很多日志,就会很烦。
闭包可以先把前缀固定住:
def make_logger(prefix):
def log(message):
print(f"[{prefix}] {message}")
return log
order_log = make_logger("订单")
pay_log = make_logger("支付")
order_log("创建成功")
order_log("开始校验库存")
pay_log("扣款成功")
pay_log("发送支付通知")
这样每个模块拿到的是一个已经配好前缀的日志函数。
这就是闭包在工程里特别实用的一个点:
先做一次配置
后面调用时更干净
闭包为什么能让代码更像“定制化工具”
因为闭包不是只有函数代码本身,它还携带状态。
普通函数像一把通用扳手。
闭包更像是给你量身调过尺寸的扳手。
同样是 log,order_log 和 pay_log 看起来是同一套逻辑,但它们内部记住的 prefix 不一样。
这就让它们变成了两个“行为一致、配置不同”的专属工具。
这正是闭包最值得用的地方:
不是为了玩语法,而是为了制造这类“带私有配置的小工具”。
很多人第一次写闭包,最容易踩的坑:循环变量晚绑定
这是闭包里最经典的坑,必须单独讲清楚。
看代码:
funcs = []
for i in range(3):
def f():
return i
funcs.append(f)
print(funcs[0]())
print(funcs[1]())
print(funcs[2]())
很多人以为输出会是:
0
1
2
但实际通常是:
2
2
2
为什么。
因为这些函数并不是在定义时把 i 的值复制进去了。
它们引用的是同一个外部变量 i。
等循环结束时,i 已经变成了 2。
所以后面调用时,它们看到的都是最后那个 i。
这个现象本质上叫晚绑定。
也就是闭包引用的不是“当时的值”,而是“那个变量本身”。
这个坑怎么解决
最常见的解决办法,是在函数定义时把当前值提前固定下来。
方法一,用默认参数:
funcs = []
for i in range(3):
def f(i=i):
return i
funcs.append(f)
print(funcs[0]())
print(funcs[1]())
print(funcs[2]())
这次输出就是:
0
1
2
因为这里的 i=i 在函数定义时就把当前值保存下来了。
方法二,用工厂函数再包一层:
def make_func(i):
def f():
return i
return f
funcs = []
for i in range(3):
funcs.append(make_func(i))
print(funcs[0]())
print(funcs[1]())
print(funcs[2]())
这个思路也很常见,而且更符合闭包本身的设计方式。
以后只要看到:
循环里创建函数
函数里又引用了循环变量
你就要立刻提高警惕。
这是闭包里最容易出错的地方之一。
为什么默认参数能修这个坑
因为默认参数的值是在函数定义时计算的,而不是调用时计算的。
也就是说:
def f(i=i):
return i
这里右边那个 i,是当前循环这一轮的值。
它会在定义函数时被存进参数默认值里。
所以之后就算外面的 i 继续变化,这个函数自己保存的默认参数值也不会变。
这招看起来像技巧,但本质上还是一句话:
别让所有函数都去共享同一个外部变量
要让每个函数拿到自己的独立值
闭包里保存的是状态,不是历史快照
这一点也要分清。
比如:
def outer():
items = []
def add(x):
items.append(x)
return items
return add
f = outer()
print(f(1))
print(f(2))
print(f(3))
输出是:
[1]
[1, 2]
[1, 2, 3]
这里 items 并不是每次调用都会回到初始状态。
而是一直在同一个闭包状态里累积。
这说明闭包保存的是持续存在的状态,不是一张静态照片。
也正因为如此,闭包特别适合做:
计数器
累加器
缓存器
配置好的处理器
状态型回调
这些都依赖“多次调用之间保留上下文”。
再看一个闭包实现缓存的小例子
假设有个函数计算代价比较高,你想做个简单缓存。
def make_cache():
cache = {}
def get_square(x):
if x not in cache:
print(f"第一次计算 {x}")
cache[x] = x * x
return cache[x]
return get_square
square = make_cache()
print(square(4))
print(square(4))
print(square(5))
print(square(5))
这里 cache 就被闭包保存住了。
每次调用 square,都能继续访问之前已经算过的结果。
你会发现,这种场景用全局变量也能做,用类也能做。
但闭包写起来非常轻,特别适合这种“小范围、专用型、带状态”的逻辑。
闭包的一个优点:把状态藏起来
看计数器例子:
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
外部拿到的是 counter 这个函数。
它并不能直接随便改 count。
只能通过调用函数来间接影响状态。
这比全局变量更安全。
因为状态没有裸露在外面。
所以闭包还有一个很实用的价值:
它可以做轻量级封装
把不想暴露的状态藏在函数内部
这和面向对象里的封装思想,其实是相通的。
闭包适合什么,不适合什么
适合的场景:
状态很少
逻辑集中
想生成定制化函数
想保留少量私有状态
想做轻量封装
想写回调工厂、规则工厂、日志工厂、校验工厂
不太适合的场景:
状态很多
需要多个方法协作
逻辑很复杂
后续维护者不熟悉闭包
需要很强的可读性和对象结构表达
这时候通常类会更清楚。
闭包不是越多越高级。
它是一把很锋利的小刀,适合切小而精的东西。
拿它去砍大树,就不一定合适了。
闭包和装饰器为什么关系这么近
因为装饰器本质上就是一层又一层函数包裹,而里面通常会用到外层变量。
看一个非常简化的例子:
def decorator(func):
def wrapper():
print("执行前")
result = func()
print("执行后")
return result
return wrapper
这里 wrapper 引用了外层的 func。
当 decorator 返回 wrapper 时,这本质上就已经是闭包了。
所以你后面学装饰器时会发现,闭包理解透了,装饰器就顺很多。
因为装饰器底层其实非常依赖这种“函数带着外层状态继续活下去”的能力。
写闭包时,有几个很实用的判断问题
第一,我是不是想让一个函数记住某些配置。
如果是,闭包很可能适合。
第二,我是不是需要在多次调用之间保留状态。
如果是,闭包很可能适合。
第三,这个状态是不是很轻。
如果就一两个变量,闭包通常很好用。
第四,我是不是只是想快速做一个轻量、私有、定制化的小函数。
如果是,闭包很顺手。
第五,如果状态和行为已经变复杂了,是不是该换成类。
这一步也很重要。
不要为了用闭包而用闭包。
而是看问题形状适不适合。
用一个更完整的业务例子,把闭包串起来
假设你在做接口限流。
想做一个简单版本:同一个处理器,每调用一次就计数,超过上限后拒绝。
可以这样写:
def make_limiter(limit):
count = 0
def allow():
nonlocal count
if count >= limit:
return False
count += 1
return True
return allow
login_limiter = make_limiter(3)
pay_limiter = make_limiter(2)
print(login_limiter())
print(login_limiter())
print(login_limiter())
print(login_limiter())
print(pay_limiter())
print(pay_limiter())
print(pay_limiter())
这里:
limit 是固定配置
count 是持续状态
allow 是实际暴露出去的处理函数
不同 limiter 之间互不干扰,各自带着自己的状态。
这就是闭包在真实业务中很有代表性的用法:
配置和状态一起绑定,形成专属函数实例。
把这一章压缩成一句最实用的话
闭包不是“函数里再套个函数”这么简单。
闭包真正厉害的地方,是它能让函数带着外层状态长期存活,从而生成有记忆、有配置、可定制的小工具。
你以后再看闭包,不要只想定义。
要想它能解决什么问题:
能不能帮我保存状态
能不能帮我固定配置
能不能帮我生成一组相似但不同的函数
能不能让我少用全局变量
能不能做一个轻量封装
一旦开始这样想,闭包就从概念变成工具了。
本章小结
闭包的本质,是内层函数引用了外层函数的局部变量,并在外层函数结束后仍然能继续使用这些变量。它最大的价值,不是语法炫技,而是保存状态、固定配置、生成定制函数和做轻量封装。状态简单时,闭包往往比全局变量更稳,比类更轻。
更多推荐


所有评论(0)