前两天技术群里有人问了一个挺有意思的问题:同样是嵌套函数,为什么有的能记住外层变量,有的用一次就“失忆”?顺着这个问题往下查,所有的线索最终都指向一个平时不太起眼的底层结构——Python 的 cell 对象。
cell 对象是 CPython 实现闭包时的核心机制之一。你写的每个闭包、每个装饰器,只要内部函数引用了外部函数的局部变量,背后就一定会有 cell 对象在维持这条引用通道。可惜大多数 Python 教程把闭包讲成了“外层套内层”的语法游戏,很少有人真正打开函数对象内部看看到底存了什么。这篇文章我会把 cell 对象的来龙去脉、读取方式、修改技巧,以及藏在它背后的各种坑一次讲清楚,适合想进阶 Python、阅读框架源码、写复杂装饰器的同学。
1. cell对象是什么:先从一次闭包困惑聊起
1.1 一个立刻能感受到差异的例子
我们从一个最常见的计数器闭包开始:
python复制def outer():
count = 0
def inner():
nonlocal count
count += 1
return count
return inner
c1 = outer()
c2 = outer()
print(c1()) # 1
print(c2()) # 1
print(c1()) # 2
print(c1.__closure__) # (<cell at 0x...: int object at 0x...>,)
c1 和 c2 看起来都是同一个 outer 生出来的函数,但它们互不影响。c1 调用两次得到 1 和 2,c2 一上来还是 1。为什么会这样?秘密就藏在那行 c1.__closure__ 的打印结果里:c1 身上挂着一个 cell 对象,这个 cell 才是真正保存 count 的地方。
再看一个更容易踩坑的例子:
python复制funcs = []
for i in range(3):
funcs.append(lambda: i)
for f in funcs:
print(f())
这段代码打印的不是 0、1、2,而是三个 2。原因是闭包捕获的是变量 i 本身,不是 i 当时的“值”。循环结束后 i 停留在 2,三个函数共享的是同一个 i 的 cell,所以看到的都是 2。这里面的“同一个 cell”值得你停下来细品,理解了它,闭包的很多怪现象就都能解释通了。
1.2 为什么Python需要cell对象
聊到 cell 对象的必要性,得先想想:如果没有它,Python 要怎么实现闭包?
外层函数 outer 调用结束时,它的栈帧会被销毁,局部变量 count 按理说也要跟着消失。但如果内部函数 inner 之后还要操作 count,就必须把这个变量“抢救”出来。早期 Python 没有专门的机制,只能通过默认参数复制一份值,或者干脆用全局变量。但这两种方案的缺陷都很明显:
- 默认参数只能捕获当前的“值”,做不到修改后让外层感知,也无法让多个函数共享同一个变量。
- 全局变量倒是共享了,但污染了全局命名空间,也破坏了封装。
所以 CPython 引入了 cell 这个概念:一个独立存活的小容器,专门存放那些被内部函数引用的外层局部变量。函数栈帧销毁后,只要还有函数对象引用这个 cell,cell 里的值就不会丢。你可以把它理解成一个“托管盒子”,函数不直接持有变量的值,而是持有一把打开盒子的钥匙。这个设计既保留了闭包的语义,又避免了全局命名空间的污染。
在 C 语言层面,cell 对应的是 PyCellObject 结构体,内部就是一个对象指针。Py
