很多人学闭包,卡住的不是语法,而是不知道它到底有啥用

一提到闭包,不少人脑子里会立刻冒出几个词:

内层函数
外层变量
记住状态
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 之间互不干扰,各自带着自己的状态。
这就是闭包在真实业务中很有代表性的用法:

配置和状态一起绑定,形成专属函数实例。

把这一章压缩成一句最实用的话

闭包不是“函数里再套个函数”这么简单。
闭包真正厉害的地方,是它能让函数带着外层状态长期存活,从而生成有记忆、有配置、可定制的小工具。

你以后再看闭包,不要只想定义。
要想它能解决什么问题:

能不能帮我保存状态
能不能帮我固定配置
能不能帮我生成一组相似但不同的函数
能不能让我少用全局变量
能不能做一个轻量封装

一旦开始这样想,闭包就从概念变成工具了。

本章小结

闭包的本质,是内层函数引用了外层函数的局部变量,并在外层函数结束后仍然能继续使用这些变量。它最大的价值,不是语法炫技,而是保存状态、固定配置、生成定制函数和做轻量封装。状态简单时,闭包往往比全局变量更稳,比类更轻。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐