很多初学 Python 的朋友都会有一个疑问:
函数执行完以后,里面的局部变量不是应该消失了吗?
那为什么有些函数还能“记住”之前的数据?
这个问题背后,涉及 Python 里一个非常重要的概念:闭包。闭包听起来有点抽象,但它其实并不神秘。简单来说,闭包就是:
一个函数记住了它定义时所在环境中的变量。
一、先看一段容易猜错的代码
我们先来看一段代码:
def outer():
x = 10
def inner():
nonlocal x
x += 1
return x
return inner
print(outer()())
print(outer()())
你觉得输出结果是什么?
很多人看到这种“函数里面嵌套函数”的写法,会马上想到闭包。
于是可能会认为:
inner()会记住外层函数里的变量x,
第一次调用时x从 10 变成 11,
第二次调用时x应该从 11 变成 12。
所以很多人的第一反应是输出:
11
12
但实际结果是:
11
11
这就有点奇怪了。不是说闭包可以记住外层变量吗?为什么这里看起来没有记住呢?我认为这其实正是理解闭包的关键之处。继续看下去吧,不难。
二、为什么 outer()() 两次都是 11?
关键在于:
print(outer()())
print(outer()())
这两行代码中,outer() 被调用了两次,每调用一次 outer(),都会创建一套全新的局部环境。也就是说,每次执行 outer() 时,都会重新创建一个新的 x:
x = 10
同时,也会重新创建一个新的 inner() 函数。所以第一行:
print(outer()())
可以理解成:
temp_func = outer()
print(temp_func())
这里发生了什么?
第一步,调用 outer():
temp_func = outer()
这时创建了一个新的 x,初始值是 10,然后返回一个 inner 函数。
第二步,调用这个返回的函数:
print(temp_func())
于是 x += 1,结果变成 11。
所以第一行输出:
11
第二行:
print(outer()())
又重新调用了一次 outer()。
这一次不是继续使用上一次的 x,而是又创建了一个全新的 x = 10。
所以第二次也是:
10 + 1 = 11
因此输出结果是:
11
11
可以用一个比喻来理解:
每次执行
outer()(),就像重新买了一台新的计数器。
每台计数器的初始值都是 10。
你每次只按了一下,所以每次都是 11。
所以这里并不是闭包失效了,而是你每次都创建了一个新的闭包。它们之间互不影响。
三、正确用法:先把返回的函数保存下来
如果我们希望闭包真正“记住上一次的值”,就不能每次都重新调用 outer()。
正确的写法是:
def outer():
# 外层函数中的局部变量
# 正常情况下,outer() 执行结束后,x 应该被销毁
# 但因为 inner() 使用了它,所以它会被闭包保存下来
x = 10
def inner():
# nonlocal 表示:
# 这里使用的 x 不是 inner() 自己的局部变量,
# 而是外层函数 outer() 里的 x。
#
# 易错点:
# 如果要在 inner() 里面修改外层变量 x,
# 就必须写 nonlocal x。
# 否则 Python 会把 x 当成 inner() 内部的新局部变量,
# 从而导致报错。
nonlocal x
# 每调用一次 inner(),都会让同一个 x 加 1
x += 1
# 返回修改后的 x
return x
# 返回 inner 函数本身,而不是执行 inner()
#
# 易错点:
# 这里写的是 return inner,不是 return inner()
#
# return inner:
# 把函数本身返回出去,后面可以继续调用。
#
# return inner():
# 立刻执行 inner 函数,并返回执行结果。
return inner
# outer() 执行后,返回 inner 函数
# counter 接住了这个 inner 函数
#
# 关键点:
# counter 保存的是一个“带着 x 的函数”
# 这就是闭包
counter = outer()
# 第一次调用 counter()
# 实际上是在调用 inner()
# x 从 10 变成 11
print(counter())
# 第二次调用的还是同一个 inner()
# 所以 x 会在上一次的基础上继续加 1
# x 从 11 变成 12
print(counter())
# 第三次调用的仍然是同一个 inner()
# x 从 12 变成 13
print(counter())
输出结果:
11
12
13
这段代码的关键不是 inner() 里面的逻辑有多复杂,而是这一句:
counter = outer()
这一句做了两件事:
第一,执行 outer()。
第二,把 outer() 返回的 inner 函数保存到变量 counter 中。
从这以后,我们每次执行:
counter()
其实都是在调用同一个 inner() 函数。
既然是同一个 inner() 函数,那么它引用的也是同一个 x。
所以 x 会持续累加:
10 -> 11 -> 12 -> 13
而不是每次都从 10 重新开始。
四、到底什么是闭包?
闭包其实并不神秘。
在 Python 中,如果满足下面几个条件,就可以形成闭包:
- 有一个外层函数;
- 外层函数里面定义了一个内层函数;
- 内层函数使用了外层函数中的变量;
- 外层函数把内层函数返回出去;
- 返回出去的内层函数仍然可以访问外层函数的变量。
例如:
def outer():
x = 10
def inner():
return x
return inner
这里的 inner 就是一个闭包。
因为:
inner定义在outer里面;inner使用了外层函数中的变量x;outer把inner返回了出去;- 即使
outer()执行结束,inner仍然能访问x。
我们可以把闭包理解成:
闭包 = 函数 + 它记住的外部变量
或者更形象一点:
闭包就像一个随身带着“记事本”的函数。
函数负责执行逻辑,记事本负责保存数据。
在计数器例子中:
counter = outer()
得到的 counter 就不是一个普通函数。
它是一个带着变量 x 的函数。
这个 x 就是它的“记忆”。
五、为什么外层函数结束后,变量还活着?
很多人最疑惑的地方是:
x明明是outer()里的局部变量,为什么outer()执行完之后,x没有被销毁?
按照普通理解,函数执行结束后,里面的局部变量应该被清理。
这句话本身没有错。
但是闭包是一个特殊情况。
原因是:
外层函数中的变量被内层函数引用了。
在普通函数里,局部变量只在函数执行期间存在。
例如:
def test():
a = 100
return a
当 test() 执行结束后,如果没有其他地方引用 a,那么这个局部变量对应的对象就可能被回收。但是在闭包中情况不同。
例如:
def outer():
x = 10
def inner():
return x
return inner
func = outer()
这里 outer() 虽然执行完了,但是它返回的 inner 被 func 保存了下来。
而 inner 又引用了外层变量 x。
所以 Python 不能直接把 x 清理掉。
因为 inner 以后可能还要用它。
六、闭包背后的 cell 对象
Python 为了让内层函数在外层函数结束后仍然能访问外层变量,会把这些被引用的变量保存到一个特殊的对象里。
这个对象通常叫做 cell 对象。
可以简单理解成:
func / counter
↓
inner 函数
↓
闭包中的 cell 对象
↓
外层变量 x
也就是说,x 并不是简单地作为普通局部变量存在。
它被 Python 放进了闭包相关的 cell 对象中。
只要内层函数还存在,这个 cell 对象就还存在。
只要 cell 对象还存在,里面保存的 x 就还存在。
所以,闭包能记住外层变量,并不是魔法。
它背后是 Python 帮我们保存了这些变量。
七、再从垃圾回收机制理解一下
在 Python 中,对象什么时候会被回收,主要取决于它还有没有被引用。
Python 的内存管理中有一个非常重要的机制,叫做 引用计数。
简单来说:
如果一个对象还被其他变量、函数或对象引用着,它就不会被当成垃圾回收。
来看这个例子:
counter = outer()
执行完之后,引用关系大概是这样的:
counter
↓
inner 函数
↓
闭包中的 cell 对象
↓
x 对应的对象
所以:
- 只要
counter还存在,inner函数就还存在; - 只要
inner函数还存在,它引用的闭包变量就还存在; - 只要闭包变量还存在,
x对应的对象就不会被回收。
因此,虽然 outer() 已经执行结束了,但 x 并不会马上消失。
因为在 Python 看来:
x还“有人用”,它不是垃圾。
只有当这条引用链断开时,相关对象才有机会被回收。
例如:
del counter
当 counter 被删除后,如果没有其他变量再引用这个闭包函数,那么 inner 函数以及它保存的闭包变量 x,才有机会被 Python 的垃圾回收机制清理掉。
所以要注意:
闭包的本质不是“局部变量永远不会销毁”。
而是“局部变量被内层函数引用后,会随着闭包一起继续存在”。
只要闭包还被外部引用,这些变量就不会被回收。
八、用 __closure__ 看看闭包里保存了什么
我们可以通过函数的 __closure__ 属性,查看一个函数是否携带了闭包变量。
例如:
def outer():
x = 10
def inner():
return x
return inner
func = outer()
print(func.__closure__)
print(func.__closure__[0].cell_contents)
输出类似:
(<cell at 0x000001F2A3B4C5D0: int object at 0x00007FF...>,)
10
这里的:
func.__closure__
保存的就是闭包引用的变量信息。
而:
func.__closure__[0].cell_contents
可以查看 cell 对象里具体保存的内容。
在这个例子中,看到的是:
10
这就说明:
outer()虽然已经执行结束了,但变量x仍然被inner函数记住了。
如果一个函数没有形成闭包,那么它的 __closure__ 通常是 None。
例如:
def hello():
return "hello"
print(hello.__closure__)
输出:
None
说明这个函数没有携带外部变量。
九、nonlocal 是做什么用的?
在前面的计数器代码里,我们用了 nonlocal:
def outer():
x = 10
def inner():
nonlocal x
x += 1
return x
return inner
为什么要写:
nonlocal x
原因是:
我们在
inner()里面不仅要读取x,还要修改x。
如果只是读取外层变量,不需要写 nonlocal。
例如:
def outer():
name = "Python"
def inner():
return name
return inner
这里 inner() 只是读取 name,没有修改它,所以不需要 nonlocal。
但是如果你要修改外层变量,就必须写 nonlocal。
如果不写,会发生什么?
def outer():
x = 10
def inner():
x += 1
return x
return inner
counter = outer()
print(counter())
运行后会报错:
UnboundLocalError: local variable 'x' referenced before assignment
为什么?
因为 Python 在分析函数时,只要看到函数内部有赋值操作,比如:
x += 1
它就会认为 x 是当前函数 inner() 的局部变量。
但问题是:
x += 1
本质上相当于:
x = x + 1
右边需要先读取 x,然后再加 1,最后赋值给 x。
可是 Python 已经把 x 当成 inner() 的局部变量了,而这个局部变量在读取之前还没有被赋值。
所以就报错了。
加上:
nonlocal x
就是明确告诉 Python:
这个
x不是inner()里的局部变量,
而是外层函数outer()里的变量。
我要修改的是外层那个x。
所以总结一下:
只读取外层变量,不需要
nonlocal。
要修改外层变量,必须使用nonlocal。
十、return inner 和 return inner() 的区别
闭包里还有一个非常容易出错的地方:
return inner
和:
return inner()
这两个写法差别非常大。
1. return inner
def outer():
x = 10
def inner():
return x
return inner
这里返回的是函数本身。
所以外部可以这样接收:
func = outer()
print(func())
也就是说,func 现在就是 inner 函数。
你可以以后再调用它。
这正是闭包常见的写法。
2. return inner()
def outer():
x = 10
def inner():
return x
return inner()
这里返回的不是函数,而是 inner() 的执行结果。
也就是说,outer() 执行时,会马上调用 inner(),然后把结果返回出去。
例如:
result = outer()
print(result)
输出:
10
这时 result 保存的是数字 10,不是函数。
所以你不能再写:
result()
否则会报错:
TypeError: 'int' object is not callable
所以一定要记住:
return inner:返回函数本身,形成闭包。
return inner():立即执行函数,返回执行结果。
十一、闭包有什么用?
闭包最常见的作用是:保存状态。
也就是说,让一个函数在多次调用之间,保留一些数据。
1. 用闭包实现计数器
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
counter = make_counter()
print(counter())
print(counter())
print(counter())
输出:
1
2
3
这里的 count 就是被闭包保存起来的状态。
它不会暴露成全局变量,也不会被外部随意修改。
如果不用闭包,我们可能会写成全局变量:
count = 0
def counter():
global count
count += 1
return count
这样虽然也能实现计数,但 count 是全局变量,任何地方都可以修改它,代码变复杂后就容易出问题。
闭包的好处是:
既能保存状态,又能避免使用全局变量。
2. 用闭包生成定制函数
比如我们想生成一个“乘法器”:
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10))
print(triple(10))
输出:
20
30
这里:
double = make_multiplier(2)
得到的是一个记住了 n = 2 的函数。
所以:
double(10)
相当于:
10 * 2
而:
triple = make_multiplier(3)
得到的是一个记住了 n = 3 的函数。
所以:
triple(10)
相当于:
10 * 3
这就是闭包的一个典型应用:
根据不同参数,生成不同功能的函数。
3. 用闭包封装私有变量
闭包还可以用来隐藏数据,避免变量被外部直接修改。
例如,我们写一个简单的钱包:
def create_wallet():
money = 100
def pay(amount):
nonlocal money
if amount <= money:
money -= amount
return f"支付成功,余额:{money}"
else:
return "余额不足"
return pay
wallet = create_wallet()
print(wallet(30))
print(wallet(50))
print(wallet(30))
输出:
支付成功,余额:70
支付成功,余额:20
余额不足
在这个例子中,money 是外层函数里的变量。
外部不能直接访问它。
我们不能直接写:
money = 100000
去修改闭包内部的 money。
外部只能通过:
wallet(30)
间接操作它。
这就实现了一种简单的数据封装。
当然,在 Python 中,更常见的封装方式是类。
但闭包也可以在一些轻量场景下,起到类似“私有变量”的效果。
十二、闭包的优点和缺点
闭包很好用,但也不是没有代价。
优点一:可以保存状态
闭包可以让函数记住上一次执行后的数据。
这非常适合:
- 计数器;
- 缓存;
- 配置函数;
- 回调函数;
- 装饰器。
例如计数器中的 count,就会随着函数多次调用一直保存下来。
优点二:可以减少全局变量
如果不用闭包,很多状态可能会被写成全局变量。
全局变量一多,代码就容易混乱。
而闭包可以把变量藏在函数内部,只通过返回的内层函数来操作它。
这样可以减少变量污染,也能降低被误修改的风险。
优点三:可以生成定制函数
通过外层函数传入不同参数,可以生成不同功能的函数。
比如:
double = make_multiplier(2)
triple = make_multiplier(3)
这两个函数逻辑相似,但携带的参数不同,所以行为也不同。
这在函数式编程中非常常见。
缺点一:可能增加内存占用
闭包会让外层函数中的某些变量继续存在。
如果闭包引用了很大的对象,并且闭包一直没有被释放,那么这些对象也不会被垃圾回收。
例如:
def outer():
big_data = [0] * 10000000
def inner():
return len(big_data)
return inner
func = outer()
只要 func 还存在,big_data 就不会被回收。
所以闭包虽然方便,但也要注意不要无意中长期引用大对象。
缺点二:逻辑可能不够直观
闭包中的变量生命周期比普通局部变量更复杂。
普通局部变量一般随着函数执行结束就消失。
但闭包变量会因为被内层函数引用而继续存在。
对于初学者来说,如果闭包嵌套得太多,代码就会变得不好理解。
所以闭包适合用在确实需要“保存状态”或者“生成函数”的场景中。
不建议为了炫技而滥用。
十三、再回头看最开始的问题
现在我们再回头看最开始这段代码:
def outer():
x = 10
def inner():
nonlocal x
x += 1
return x
return inner
print(outer()())
print(outer()())
为什么输出是:
11
11
因为每次执行:
outer()()
都会重新调用一次 outer()。
每次调用 outer(),都会创建一个新的 x = 10,也会创建一个新的 inner 函数。
所以两次调用之间并没有共享同一个 x。
而下面这种写法:
counter = outer()
print(counter())
print(counter())
print(counter())
输出:
11
12
13
是因为:
counter = outer()
只调用了一次 outer()。
返回的 inner 函数被 counter 保存下来。
后面每次调用:
counter()
都是在调用同一个 inner。
所以它操作的是同一个 x。
这就是闭包能够“记住状态”的真正原因。
十四、一句话总结闭包
闭包的核心可以用一句话概括:
闭包就是一个函数记住了它定义时所在环境中的变量。
普通函数执行完后,局部变量通常就结束了。
但闭包不一样。
如果外层函数中的变量被内层函数引用,并且内层函数被返回到外部继续使用,那么这些变量就会随着闭包一起继续存在。
它就像给函数装了一个“记忆胶囊”。
函数负责执行逻辑。
闭包变量负责保存状态。
如果你看到这种结构:
def outer():
x = 10
def inner():
return x
return inner
就可以想到:
inner不只是一个函数,
它还带着外层变量x的记忆。
理解了闭包,后面再学习装饰器、函数式编程、回调函数,就会轻松很多。