编译原理闭包的概念,编译原理语言的闭包和正闭包

  编译原理闭包的概念,编译原理语言的闭包和正闭包

  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

: Bycode用来执行Python,这个函数的参数要有PyFrameObject,要传入执行环境的信息。Bycode存储在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,从符号表中把这个约束存储在本地命名空间(也就是一个字典)中(不考虑这个表的具体信息,只知道这个表存储的是变量名之类的符号。上面提到的常数表存储的是变量值,这里的符号表存储的是变量名)。

  在闭包实现的过程中,字节码STORE_DEFEF用于分配与闭包相关的变量。

  先将LOAD_CONST推送的值‘1’弹出到堆栈中,存放在W中(当然这里处理的是对象);

  然后,从freevars中,我取出一些东西,放在x中,freevars值如下:

  其实这里是让freevars指向单元格对象区域。见上图FrameObject布局(f_localsplus是FrameObject的最后一个成员)。

  下一个PyCell_Set是设置一个Cell对象指向integer对象1。请注意,变量值“1”在这里已被绑定到“out_func”。

  下面两条字节码指令LOAD_CLOSER:从freevars中取出刚才(上面刚设置好的)的cell对象,堆栈起来备用。

  BUILD_Tuple:将刚刚堆叠的Cell对象(integer object 1)打包成一个TUPLE(也就是Python中TUPLE的源代码实现)并进行堆叠。

  接下来的两个LOAD_CONST,

  第一个LOAD_CONST堆栈inter_func的CodeObjet。

  第二个LOAD_CONST是堆栈函数名字符串对象 inner_func 。

  然后,下面的MAKE _ func指令,注意运行时栈顶的string对象用 inner_func ,第二个元素用inter_func。CodeObject和带有BUILD_TUPLE的第三个元素被推入Cell对象集合。

  [代码已被删除]

  首先是inner_func的字符串。弹出CodeObject和函数名‘inner _ func’(此时栈顶是单元格元组的集合),用来生成函数对象(待返回)。

  接下来,判断操作码是否应该创建闭包(当然是在我们的例子中)(这里好像有问题)。操作是取出单元格元组集,然后通过PyFunction_SetClosure()将这个元组集绑定到inter_func对应的func object(设置func object的func_closure字段)。到目前为止,inter_func已经绑定到a的值为1的integer对象。

  然后下面的字节码把这个对象返回给上面(那个调用outer_func的“人”)。此时,获得的对象inter_func包含要使用的变量值1。这个函数对象inter_func。FunctionObject及其“约束”(可能不再叫约束)实现了闭包效应。

  执行inter_func: inter_func()时,在PyEval_EvalCodeEx中执行字节码之前,会执行以下操作。

  这里,func_closer字段中的集合(只是单元集合)被放置在inter_func堆栈帧的freevars字段中,

  当使用这个变量A的值时,还需要获取freevars字段中的值。

  

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

  总而言之,

  这个变量值的流向1:先作为outer_func堆栈框架的cellvar字段中的cell对象(供内部函数使用),然后打包成元组集(可以使用多个变量,所以我们要打包,虽然我们只用了一个),再绑定成inter_func的函数对象(相当于一直被打包);这个整体可以叫做闭包,也就是这个值可以用在inter_func中。

  使用值当然是调用inter_func。注意,上面提到的操作(前四行)是在调用outer_func时。

  调用inter_func时,字节码将被执行并进入PyEval_EvalCodeEx。如前所述,在这里,刚刚绑定到inter_func函数对象的单元格集将依次放入inter_func堆栈框架的freevars区域。当将来使用这个值时,它将从freevars字段中提取。

  这里可能有人会问,为什么只有变量值?当使用print(a)时,不存储a的值,并且不校正约束(a,1)。你如何找到整数1?

  实际上,这非常简单,因为访问这些值(包括位置参数)是通过下标进行的。比如刚才1最后放在freevars的0位置,那么取值的时候就是o=freevars[0]。我们如何确定变量的位置?当然是在编译时确定的,这个信息可以在编译时确定。

  参考号:《Python源码剖析》

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

  如有错漏,请指正。

编译原理闭包的概念,编译原理语言的闭包和正闭包