赞
踩
这里可以把闭包当成一个由两部分组成的整体?哪两部分呢:1、函数 2、”约束“(也就是引用的外部函数的变量)
举个例子:
这里把函数inner_func返回,其实返回的并不仅仅是inner_func这个函数,还有(a, 1)这个外部的变量这条约束,Python将这两个作为一个整体捆绑起来,然后把整体返回,这个捆绑的整体叫做闭包,随着下面的深入,会发现其实返回的仅仅是inner_func的函数对象,只不过(a, 1)这条约束被设置成了函数对象的某一部分, 这种说法是不太正确的,其实只把值存起来了。
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源码剖析》
这本书看了已经一年了,文章草稿也存了一年了,内容忘了差不多了,所以逻辑可能不是很连贯,
如有错漏,恳请指正。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。