闭包的实现机制
今天这篇文章我们来聊一下 Python 中闭包的实现机制。
闭包的基本形式
首先我们来简单说一下什么叫闭包。我们看下面这个代码,我们定义了一个函数 f,f 里面有一个变量,data 是一个空 list。那在 f 里面我们又定义了另外一个函数,叫做 inner。这个 inner 函数里面使用了 data 这个变量。
def f():
data = []
def inner(value):
data.append(value)
return data
return inner
g = f()
print(g(1))
print(g(2))
那很明显,对 inner 来说,data 这个变量,它不属于局部变量,因为 Python 的局部变量是以函数为单位的,对吧?这个变量并不是只在 inner 函数里面存在。同时它又不是全局变量,这个非常明显。那这种能从一个函数里,去读取其他函数变量的机制,我们就叫做闭包。
我们可以看到,这个 f 函数运行的结果,是返回这个 inner 函数。我们把这个结果保存给 g,那这个时候 g 跟 inner 都是一样的,是同一个函数。这个 inner 函数有一个参数 value,它每次会把这个 value 加到 data 这个 list 里面,然后把 data 这个 list 返回。
我们简单跑一下,打印一下 g(1),再打印一下 g(2)。可以看到,g(1) 把 1 加到这个 list 里面,并且打印出来。g(2) 把 2 又加到这个 list 里面,打印出来的是 1、2。也就是说,这两次 g 的调用里,或者说是 inner 的调用里,它们使用的 data 是同一个 data,是 f 里面我们初始化的那个 data。
外层变量重新赋值的问题
那这种机制是怎么实现的呢?有人说很简单啊,对吧?我只要在函数定义的时候,把外部函数的所有局部变量,复制一份,复制到里面这个函数,不就可以了吗?
那如果真的是在定义函数的时候,复制局部变量这么简单,你就没法解释下面这件事。我们看,我们在定义完 inner 这个函数之后,我们把 data 再重新进行一次赋值。注意啊,我们做的是对变量的重新赋值,而不是修改这个变量。如果闭包的实现机制,是简单地复制局部变量的话,我们这一次对 data 的重赋值,不应该影响到我们 inner 里面的那个 data。
def f():
data = []
def inner(value):
data.append(value)
return data
data = [0]
return inner
g = f()
print(g(1))
print(g(2))
当然了,从结果上大家可以看到,inner 里面那个 data,使用的是被重新赋值之后的这个 data。也就是说,它好像知道在 f 里面这个 data 发生了什么,它可以一直追踪这个函数 f 里面的 data 变量。
不仅如此,我们想一下,如果是简单的局部变量复制,那 data 这个值,它被保存在哪了呢?我们知道,一个函数是不会引用它的局部变量的。也就是说,一个函数只有在运行的时候,才会重新去建立这个局部变量。在运行完之后,这个局部变量就被扔掉了。
那 data 是 f 的局部变量,对吧?在我们调用 f 之后,f 这个函数对 data 的引用就应该失效了。那凭什么我们在反复调用 g 的时候,这个 data 还一直被保存在内存里呢?今天我们就带着大家来研究一下这个问题。
闭包的字节码
首先要说明的是,今天我们使用的 Python 版本,是 Python 3.13。大家知道呢,在最近这几个版本中,Python 的 bytecode 都有比较大的变化。所以不同的版本之间啊,可能有些差别,但是总体的实现方向啊,是一致的。
首先是我们的传统艺能啊,打开 bytecode。
import dis
def f():
data = []
def inner(value):
data.append(value)
return data
return inner
dis.dis(f)
大家可以看到,在 f 函数的 code object 里面,出现了一些我们平时不熟悉的 bytecode,比如说最开始的这个 MAKE_CELL,包括后面的 STORE_DEREF,这里还有 SET_FUNCTION_ATTRIBUTE、closure。而在这个 inner 函数的 code object 里面,它在读取 data 的时候,也不像读取 value 一样使用 LOAD_FAST,而是使用了 LOAD_DEREF。那这些都和我们的闭包机制息息相关。
创建 cell
我们一个一个来解读啊,从 MAKE_CELL创建一个 cell 对象,用来保存需要被嵌套作用域共享的变量绑定。 开始。我们看 MAKE_CELL 的代码非常简单啊,它的核心只有一件事,就是建立了一个 cell object。而这个 cell object,就是专门被拿来保存,被多个 scope 所引用的变量的。你可以把它理解为一个盒子,我们把变量装到这个盒子里。而在每一个 scope 里修改变量的时候,实际上修改的是盒子里面,保存的那个变量。这样就可以保证每一个 scope 看到的是同一个变量,而不是一个变量被复制了很多次。
更新 cell
那当然这里的代码啊,在 MAKE_CELL 之后,核心就是这个 STORE_DEREF把一个值写入指定 cell,更新这个 cell 保存的变量绑定。。我们看这个代码就和我们刚才说的一样,拿到这个装着变量的盒子,把现在盒子里面装着的东西拿出来,把这个新的值、新的变量装进盒子里。这样在其他的 scope 再去尝试拿盒子里东西的时候,它们就都能知道这个盒子里东西变了。
而值得注意的是,这里的 bytecode 是用 STORE_DEREF,还是 STORE_FAST,也就是针对局部变量的方式,是 Python 的编译器决定的。也就是说,在 Python 代码的编译期,Python 会识别每一个变量到底是单纯的局部变量,还是需要闭包时候用的变量。在编译期被识别,产生了不同的 bytecode,于是出现了不同的 runtime behavior。
访问 cell
那可想而知,在 inner 这个函数里,在读取 data 的时候,使用的这个 LOAD_DEREF读取指定 cell 中保存的值,供当前函数使用。,肯定也采用了相似的机制。大家可以看到,就是从 local variable 里面拿到这个 cell,再从这个 cell 里拿到实际的值。
而这些 cell 是在 COPY_FREE_VARS把函数闭包中的 cell 引用复制到当前调用帧,供 LOAD_DEREF 等指令访问;不会复制 cell 中的列表。 里面被复制过来的。当 Python 编译器发现一个函数,使用了其他函数的变量,就会跑一下 COPY_FREE_VARS。同样的变量 data,在 inner 这个函数里面,它就是一个 freevars。free 代表不是我这个函数定义的变量。而在函数 f 里,它一般被称作 cellvars。这个 cell 就取自这个 cell object。
__closure__ 保存的 cell
那我们刚才还有一个问题没有解决,对吧?就是这些变量它保存在哪了?我们知道 Python 是一个引用计数的机制。你这个 data,不管你是 cellvars 还是 freevars,一定得有一个东西对你保持引用,你才能活着。
那我们知道 data 先被放到了一个 cell 里,所以 data 本身可能有 cell 对它的引用。那问题就是谁在引用 cell,或者说 cell 活在哪了?
这时候就让我们的目光转向这个 SET_FUNCTION_ATTRIBUTE将 cell 元组设置为函数对象的闭包属性;参数 8 表示 closure。。这里有一个大大的 closure,也就是闭包的英文。我们可以看到,在 SET_FUNCTION_ATTRIBUTE 这个 bytecode 里面,它把这个 attr 赋值给了这个 function object 下面的 __closure__。而这个 attr 就是我们这里建立的这个 tuple。这个 tuple 里面包含了我们所有需要传递的 cell variable,也就是说,这些 cell variable 实际上活到了 function object 里面。
也就是说,当我们做 g 等于 f() 的时候,我们建立了一个 function object g。这个 function object 里面虽然没有保存任何的局部变量,就像我刚才说的,你只有调用这个 function object 才会产生局部变量。在这个 function object 运行结束的时候,这些局部变量就没了。但是这个 function object 本身保存了闭包变量。
事实上,我们可以找到这个 function object 里面保存的 freevars,它就在这个 __closure__ attribute 里。
def f():
data = []
def inner(value):
data.append(value)
return data
return inner
g = f()
print(g.__closure__)
我们可以看到,__closure__ 里保存了一个 cell object,而这个 cell object 里面存放的是一个 list object,也就是我们的 data。因此,只要 g 还活着,就永远有对 data 的引用。我们每一次 append 也永远可以放到同一个 data 里。
验证 cell 引用的对象
def f():
data = []
def inner(value):
data.append(value)
return data
return inner
g = f()
print(g.__closure__)
print(g(1))
print(g(2))
print(hex(id(g(3))))
print(g.__closure__[0].cell_contents)
print(g.__closure__[0].cell_contents is g(4))
我们可以看到,__closure__ 里面保存的这个 list object 和返回回来这个 data list,它们俩的 id 是一致的。
闭包与全局变量的对比
那闭包这个事情它本身呢,没什么神秘的。从结果上看,它只是建立了一个新的 scope。如果我们不看第一行这个 def f,其他的代码就瞬间变得好理解了很多。data 就是一个全局变量嘛,那 inner 就是一个函数使用了一个全局变量嘛。那如果这个全局变量在定义完 inner 之后改变了,那 inner 在 access 这个全局变量的时候,当然也会拿到不同的值。这非常容易理解,对吧?
data = []
def inner(value):
data.append(value)
return data
print(inner(1))
print(inner(2))
print(inner.__closure__)
所谓闭包,形式上,就是让 f 这个函数成为了一个新的小全局。在函数 f 里可以定义属于这个函数 f 的全局变量,然后所有定义在这个函数里的其他函数,都可以去使用这个全局变量。当然它比较神奇的地方在于,即使 f 的调用结束了,甚至对 f 的引用都没有了,这个闭包变量它依然能活着。