Python 闭包原理

1、什么是闭包?

这里可以把闭包当成一个由两部分组成的整体?哪两部分呢:1、函数  2、”约束“(也就是引用的外部函数的变量)

举个例子:

这里把函数inner_func返回,其实返回的并不仅仅是inner_func这个函数,还有(a, 1)这个外部的变量这条约束,Python将这两个作为一个整体捆绑起来,然后把整体返回,这个捆绑的整体叫做闭包,随着下面的深入,会发现其实返回的仅仅是inner_func的函数对象,只不过(a, 1)这条约束被设置成了函数对象的某一部分, 这种说法是不太正确的,其实只把值存起来了。

2、PyCodeObject, PyFrameObject, PyFunctionObject, PyEval_EvalFrameEx(), PyEval_EvalCodeEx()

PyCodeObject对应Python的一个作用域,也就是说一个作用域会有一个独立的PyCodeObject对象,这个对象是Python编译的结果,这个对象中存放了编译产生的信息,比如字节码、常量、变量名等,在这个对象的相关域中存放。

这个对象的co_code域中存放编译产生的字节码,co_freevars和co_cellvars与闭包有关,co_cellvars存放的是嵌套函数所使用的变量集合,比如上面的代码中,outer_func对应的PyCodeObject对象的co_cellvars就会存放a这个变量的值(因为嵌套函数inner_func使用了这个变量(print(a)) ),而co_freevars存放的是使用的外部函数的变量值的集合,比如inner_func对应的PyCodeObject对象的co_freevars就会保存变量a的值。

PyFrameObject: 就是执行环境,也是一个对象,也叫栈帧;与PyCodeObject也是对应的,PyFrameObject的f_code域就是存放与它对应的PyCodeObject对象的;

PyFunctionObject:函数对象,当执行def func(): 时,就会创建一个PyFunctionObject对象

在这里我们仅关注func_closure这个域,因为它与闭包的实现有关, (看名字也能看得出来).

PyEval_EvalFrameEx:用于执行Python的字节码,而这函数的参数就要有一个PyFrameObject,要把执行环境的相关信息传入,而PyFrameObject对应的PyCodeObject对象中存放有字节码,所以PyEval_EvalFrameEx与PyEval_EvalCodeEx得以执行这些字节码,当然这两个函数不仅需要PyFrameObject这一个参数。

PyEval_EvalCodeEx

PyEval_EvalCodeEx:会先进行大量与参数有关的处理,最后还是会调用上面的PyEval_EvalFrameEx()函数,进行字节码指令的执行。

 

由于outer_func()这个函数的某些原因(进行了某些判断(由于闭包)),这个函数并不能直接进入PyEval_EvalCodeEx(),而是进入了PyEval_EvalCodeEx()中进行相关处理,然后才可以进入 PyEval_EvalCodeEx执行outer_func编译出的字节码。

 

我们先来看一下在真正执行这outer_func函数的字节码之前,在PyEval_EvalCodeEx()中进行了哪些处理:

在介绍之前,我们先要知道PyFrameObject的布局:

下面给出outer_func在被调用时的字节码:

其中第一部分对应a=1; 第二部分对应def inner_func; 第三部分比较简单,就是返回inner_func;

先来看第一部分:a = 1对应的字节码,可能大家一眼看不出来这两条字节码指令有什么特别的,那让我们来对比一下普通的赋值语句;

这是b=2这个普通赋值语句的字节码(注意虽然a=1也是赋值,但是a这个变量与闭包有关,所以会与普通的赋值不同),

b=2这两条语句的字节码意思分别是:

LOAD_CONST: 从常量表中取出2这个值(在这里不用去追究这个常量表在哪,长什么样,这不是我们的重点),取出后压入栈(在这里只需要知道了把值压入到了某个栈中就好了)

STORE_NAME: 从栈中取出2这个值,并且从符号表(也不用管这个表的具体信息,只需要知道,这个表中存放了变量名等符号,上面说的常量表是存变量值的,而这里的符号表是存变量名的),然后把这条约束存入local名字空间(就是个字典)。

而在闭包的实现过程中,与闭包有关的变量的赋值,使用的是STORE_DEFEF这条字节码,

首先弹出LOAD_CONST压入栈的值‘1’,存放到w中(当然这里处理的是对象);

然后从freevars,取出了一个东西放到了x中,freevars值如下:

, 其实这里就是让freevars指向Cell对象区域,见上面FrameObject布局(f_localsplus为FrameObject最后一个成员)。

下一条PyCell_Set就是设置了一个Cell对象指向整数对象1, 注意这里已经把变量值‘1’与out_func”绑定“起来了。

下面两条字节码指令LOAD_CLOSER: 从freevars取出刚才的cell对象(上面刚刚设置好),并压栈准备后面使用。

BUILD_TUPLE:把刚压栈的Cell对象(整数对象1),打包成一个Tuple(就是Python中元组的源码实现), 并压栈。

再下面两个LOAD_CONST,

第一个LOAD_CONST把inter_func的CodeObjet压栈。

第二个LOAD_CONST是把"inner_func"这个函数名字符串对象压栈。

然后下面MAKE_FUNCTION指令,注意这时运行时栈中栈顶为"inner_func"字符串对象,第二个元素为inter_func.CodeObject,第三个为BUILD_TUPLE压入了Cell对象集合。

[代码已删减]

首先把inner_func.CodeObject和函数名"inner_func"字符串弹出(此时栈顶为Cell元组集合),用于生成一个函数对象(要被返回)。

然后下面判断操作码是否要创建闭包(在我们的例子中当然是要创建了)(这里好像有点问题),进行的操作是取出Cell元组集合,再通过PyFunction_SetClosure(), 把这个元组集合绑定到inter_func对应的函数对象上(设置上FunctionObject的func_closure域上),到此inter_func已经与a的值1这个整数对象绑定起来了。

然后下面字节码把这个对象返回给上面(调用outer_func的”人“),这时得到的对象inter_func包含了其中要使用的变量值1, 这个函数对象inter_func.FunctionObject与其中的”约束“(可能已经不能叫约束这个名字了),实现了闭包的效果。

 

在执行inter_func时:inter_func(), 到达PyEval_EvalCodeEx中执行字节码之前会有如下操作

这里是把func_closer域中的集合(刚才的Cell集合)放到了inter_func栈帧的freevars域中,

在使用这个变量a的值时,也是要到freevars域中取值。

 

装饰器也是在闭包的基础上来实现的。

 

总结一下,

这个变量值1的流向:先在outer_func栈帧的cellvar域中作为cell对象(要被内层函数使用),然后打包成Tuple集合(可以使用多个变量,所以要打包,虽然我们只使用了一个),然后被绑定在inter_func的函数对象中(相当于打包在一直了);  这个整体可以就叫闭包, 也就是可以在inter_func中使用这个值了。

使用值当然是要调用inter_func了,注意上面(前边四行)说的操作是调用outer_func时的,

调用 inter_func时,就会执行字节码进入PyEval_EvalCodeEx中,如前所述,在这里把刚才绑定到inter_func函数对象中的Cell集合依次放到inter_func栈帧的freevars区域中,以后使用这个值时,到freevars域中来取。

这里可能有人会问,为什么只有变量值呢,那print(a)时,没有存a的值,更没有更这对约束(a, 1), 怎么找到1这个整数呢?

这其实很简单,因为访问这些值,包括位置参数都是通过下标来访问的,比如刚才的1最终放到了freevars的0号位置,那么取值时就是 o = freevars[0]了,那又怎么确定变量位置呢,当然是编译时就确定的,这些信息编译时完成可以确定。

 

参考:《Python源码剖析》

这本书看了已经一年了,文章草稿也存了一年了,内容忘了差不多了,所以逻辑可能不是很连贯,

如有错漏,恳请指正。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值