9.2 构建帧对象
frame object 概述
编译后的 code object 会被添加到 frame object 中。由于 frame object 是一种 Python 类型,因此它可以被 C 或 Python 引用。执行 code object 中的指令时还需要其他运行时数据,这些数据也包含在 frame object 中,例如局部变量、全局变量和内置模块。
帧对象类型
frame object 的类型是 PyObject
,它包含了以下属性:
f_back
PyFrameObject *
指向栈中前一个帧的指针,如果是第一帧则值为 NULL
f_code
PyCodeObject *
需要执行的 code object
f_builtins
PyObject * (dict)
内置(builtin
)模块的符号表
f_globals
PyObject * (dict)
全局变量的符号表(PyDictObject
)
f_locals
PyObject * (dict)
局部变量的符号表
f_valuestack
PyObject **
指向最后一个局部变量的指针
f_stacktop
PyObject **
指向 f_valuestack
中下一个空闲的插槽(slot)
f_trace
PyObject *
指向自定义追踪函数的指针(请参阅"帧的执行追踪")
f_trace_lines
char
切换自定义追踪函数在行号级别进行追踪
f_trace_opcodes
char
切换自定义追踪函数在操作码级别进行追踪
f_gen
Pybject *
借用的生成器引用或 NULL
f_lasti
int
上一条执行的指令
f_lineno
int
当前行号
f_iblock
int
当前帧在 f_blockstack
中的索引
f_executing
char
标记帧是否仍在执行
f_blockstack
PyTryBlock[]
保存 for
块,try
块 和 loop
块的序列
f_localsplus
PyObject *[]
局部变量和栈的联合
相关的源文件
与 frame object 相关的源文件如下:
Objects/frameobject.c
frame object 的实现和 Python API
Include/frameobject.h
frame object 的 API 和类型定义
Frame Object 初始化 API
帧对象的初始化 API 是 PyEval_EvalCode()
,这也是解析代码对象的入口点。PyEval_EvalCode()
是对内部函数 _PyEval_EvalCode()
的封装。
本节将逐步带你理解 _PyEval_EvalCode()
的执行逻辑。
_PyEval_EvalCode()
定义了许多参数。
tstate: 类型为
PyThreadState *
,它指向负责解析此块 code object 的线程的线程状态;_co: 类型为
PyCodeObject *
,它包含了将要放入 frame object 的 code object;globals: 类型为
PyObject*
(实际类型为一个字典),字典的键为变量名,字典的值是变量的值;locals: 类型为
PyObject*
(实际类型为一个字典),字典的键为变量名,字典的值是变量的值。
其他的参数都是一些可选项,没有在基础 API 中使用。
argcount:位置参数的数量;
args:类型为
PyObject*
(实际类型为元组),按顺序排列的位置参数;closure:类型为元组,包含了要合入 code object 中
co_freevars
字段的字符串;defcount:位置参数的默认值列表长度;
defs:位置参数的默认值列表;
kwargs:关键字参数值的列表;
kwcount:关键字参数的数量;
kwdefs:包含关键字参数默认值的字典;
kwnames:关键字参数名的列表;
name:求值语句的名称字符串;
qualname:求值语句的限定名字符串。
将帧的
f_back
属性设置为线程状态的最后一帧;将
f_code
属性设置为当前正在执行求值的 code object;将
f_valuestack
属性设置为一个空的值栈;将栈顶指针
f_stacktop
指向f_valuestack
;将全局变量属性
f_globals
的值设置为globals
参数的值;将局部变量属性
f_locals
的值设置为一个新的字典;将
f_lineno
设置为 code object 中的co_firstlineno
属性,以便于产生异常时的回溯包含行号;将其余属性都设置为它们的默认值。
使用新创建的 PyFrameObject
实例,就可以构造出 frame object 的参数:
将关键字参数转换为字典
Python 中的函数定义可以使用 **kwargs
来获取关键字参数,例如:
在这个例子中,未解析的参数将被复制到一个新创建的字典中。然后 kwargs
这个名字会被设置为帧中局部作用域的变量。
将位置参数转换为变量
每个位置参数(如果存在的话)都将被设置为局部作用域内的变量。在 Python 中,函数参数已经是函数体中的局部变量了。当给函数的位置参数赋值后,在函数作用域内就可以使用这些变量:
这些变量的引用计数将会增加,因此在帧完成求解前(例如函数结束并返回时)都不会触发垃圾回收去移除这些变量。
将位置参数打包为 *args
*args
类似于 **kwargs
,函数中以 * 开头的参数可以捕获所有剩余的位置参数。这个参数是一个元组类型的变量,并且 *args
这个名字会被设置函数作用域内的局部变量。
加载关键字参数
在使用给关键字参数赋值的方式调用函数时,如果存在参数既无法解析符号名也不是位置参数,此时可以使用一个字典去接收那些剩余的关键字参数。
下面给出一个例子,参数 e
既不是位置参数也没有预设的参数名,所以它被添加到了字典参数 **remaining
中:
添加缺失的位置参数
函数调用时,有一些位置参数并不在定义的位置参数列表中,这些参数会被添加到一个形如 *args
的元组中,如果这个元组不存在函数就会抛出异常。
添加缺失的关键字参数
函数调用时,有一些关键字参数并不在定义的关键字参数列表中,这些参数会被添加到一个形如 **kwargs
的字典中,如果这个字典不存在函数就会抛出异常。
折叠闭包
所有的闭包的名称都会被添加到 code object 的空闲变量名列表中。
创建生成器、协程和异步生成器
如果正在求值的 code object 存在标志表明它是一个生成器,协程或异步生成器,就会使用生成器、协程和异步库中特定的函数去创建一个新的帧,并将这个帧添加到当前的属性中。
接着会返回这个新产生的帧,而不是继续求解原帧的结果。只有在调用生成器、协程或异步方法时才会对这个新的帧求值。
Last updated