9.2 构建帧对象
frame object 概述
Last updated
frame object 概述
Last updated
编译后的 code object 会被添加到 frame object 中。由于 frame object 是一种 Python 类型,因此它可以被 C 或 Python 引用。执行 code object 中的指令时还需要其他运行时数据,这些数据也包含在 frame object 中,例如局部变量、全局变量和内置模块。
frame object 的类型是 PyObject
,它包含了以下属性:
字段 | 类型 | 用途 |
---|---|---|
与 frame object 相关的源文件如下:
帧对象的初始化 API 是 PyEval_EvalCode()
,这也是解析代码对象的入口点。PyEval_EvalCode()
是对内部函数 _PyEval_EvalCode()
的封装。
注
_PyEval_EvalCode()
是一个非常复杂的函数,它定义了 frame object 和解释器循环求值的很多行为。同时,它也是一个需要理解的重要函数,因为它可以教会你 CPython 解释器的一些设计原则。
本节将逐步带你理解 _PyEval_EvalCode()
的执行逻辑。
_PyEval_EvalCode()
定义了许多参数。
tstate: 类型为 PyThreadState *
,它指向负责解析此块 code object 的线程的线程状态;
_co: 类型为 PyCodeObject *
,它包含了将要放入 frame object 的 code object;
globals: 类型为 PyObject*
(实际类型为一个字典),字典的键为变量名,字典的值是变量的值;
locals: 类型为 PyObject*
(实际类型为一个字典),字典的键为变量名,字典的值是变量的值。
注
在 Python 中,局部变量和全局变量都以字典形式存储。你可以分别通过内置函数 local()
和 globals()
去访问这些变量:
其他的参数都是一些可选项,没有在基础 API 中使用。
argcount:位置参数的数量;
args:类型为 PyObject*
(实际类型为元组),按顺序排列的位置参数;
closure:类型为元组,包含了要合入 code object 中 co_freevars
字段的字符串;
defcount:位置参数的默认值列表长度;
defs:位置参数的默认值列表;
kwargs:关键字参数值的列表;
kwcount:关键字参数的数量;
kwdefs:包含关键字参数默认值的字典;
kwnames:关键字参数名的列表;
name:求值语句的名称字符串;
qualname:求值语句的限定名字符串。
接下来我们探讨如何去创建一个新的帧对象,调用 _PyFrame_New_NoTrack() 可以创建一个新帧,也可以通过 C API PyFrame_New() 间接调用此API。_PyFrame_New_NoTrack()
将按以下步骤创建一个新的 PyFrameObject
:
将帧的 f_back
属性设置为线程状态的最后一帧;
通过设置 f_builtins
属性加载已有的内置函数,同时通过 PyModule_GetDict() 加载内置模块;
将 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
类似于 **kwargs
,函数中以 * 开头的参数可以捕获所有剩余的位置参数。这个参数是一个元组类型的变量,并且 *args
这个名字会被设置函数作用域内的局部变量。
在使用给关键字参数赋值的方式调用函数时,如果存在参数既无法解析符号名也不是位置参数,此时可以使用一个字典去接收那些剩余的关键字参数。
下面给出一个例子,参数 e
既不是位置参数也没有预设的参数名,所以它被添加到了字典参数 **remaining
中:
注
限定型位置参数是 Python 3.8 中的新特性, PEP 570 中引入的限定型位置参数可以禁止用户在位置参数上使用关键字语法。
例如,下面这个简单的函数将华氏度转换为摄氏度。注意,/
会作为一个特殊的参数将限定型位置参数与其他函数参数分开。
分割符 /
左边的参数都只能以位置参数的形式调用,右边则不作限制,可以使用位置参数也可以使用关键字参数。
调用函数时,对限定型位置参数使用关键字参数语法将会抛出一个 TypeError
异常:
对关键字参数字典值的解析是在所有参数都解压之后进行的。如果在第 3 个参数上使用了 /
符号,则 code object 中 co_posonlyargcount
的值将会是 2 。所以 PEP 570 中提到的限定型位置参数可以通过将 co_posonlyargcount
作为循环的次数来获取。对剩下的参数调用 PyDict_SetItem() ,就可以将他们添加到 locals
字典中。当函数执行时,每一个关键字参数都将变为函数作用域内的局部变量。
如果在定义关键字参数时添加了默认值,那它在函数作用域内就已经可用了:
函数调用时,有一些位置参数并不在定义的位置参数列表中,这些参数会被添加到一个形如 *args
的元组中,如果这个元组不存在函数就会抛出异常。
函数调用时,有一些关键字参数并不在定义的关键字参数列表中,这些参数会被添加到一个形如 **kwargs
的字典中,如果这个字典不存在函数就会抛出异常。
所有的闭包的名称都会被添加到 code object 的空闲变量名列表中。
如果正在求值的 code object 存在标志表明它是一个生成器,协程或异步生成器,就会使用生成器、协程和异步库中特定的函数去创建一个新的帧,并将这个帧添加到当前的属性中。
参见
"并行和并发"这一章进一步介绍了生成器、协程和异步帧的 API 和实现细节。
接着会返回这个新产生的帧,而不是继续求解原帧的结果。只有在调用生成器、协程或异步方法时才会对这个新的帧求值。
最后,_PyEval_EvalFrame() 与新帧一起被调用。
文件 | 功能 |
---|---|
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 *[]
局部变量和栈的联合
Objects/frameobject.c
frame object 的实现和 Python API
Include/frameobject.h
frame object 的 API 和类型定义