【CPython3.6源码分析】PyCodeObject/PyFrameObject

参考

前言

事实上,Python虽然是解释型语言,但也需要经过 源文件 -> 编译 -> 可执行文件 -> 执行整个过程。

1
2
3
4
5
javac Example.java  -> Example.class
java Example.class -> 输出

python Example.py -> 创建或加载 PyCodeObject -> 输出
保存PyCodeObject到 -> Example.pyc

如上,编译器在编译产生 code 后由虚拟机执行。Python 与 Java 不同之处在于 Python 的虚拟机是一种抽象层次更高的虚拟机。 在 编译结束后,Python 会将 code 对象所包含的的信息存储在 pyc 文件内,下次运行直接加载 pyc 里的 code 对象到内存。

Python 解释器(interpreter),同时拥有编译器和虚拟机的身份。具体流程参见PEP3147PyCodeObject 包含 Python 虚拟机所需要的信息,而 pyc 就是其在硬盘实体化后的载体。

PyCodeObject

PyCodeObject,同样是 Python 对象,也继承 PyObject_HEAD。对于 Python 源代码中的任一 Code Block 都会创建一个 CodeObject 与之对应。一个新的命名空间,是一个 Code Block。类、函数、module都对应一个新的命名空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
typedef struct {
PyObject_HEAD
int co_argcount; /* 位置参数 *args 个数 */
int co_kwonlyargcount; /* #keyword only arguments */
int co_nlocals; /* 局部变量个数,包含位置参数 */
int co_stacksize; /* 需要的栈空间 */
int co_flags; /* 对block进行划分,详见 inspect.rst */
int co_firstlineno; /* 对应 .py 的起始行 */
PyObject *co_code; /* 字节码序列,PyStringObject 形式 */
PyObject *co_consts; /* list (所有常量) */
PyObject *co_names; /* list of strings (所有符号) */
PyObject *co_varnames; /* tuple of strings (局部变量名) */
PyObject *co_freevars; /* tuple of strings
使用了外层作用域中的变量名 */
PyObject *co_cellvars; /* tuple of strings
嵌套函数中使用了的变量名 */
/* 除了 co_name,余下都不用于 hash 或 比较。
为了回溯和debug,保留行号和名字。
否则,会不断的 覆盖已有的同名 func/lambda
*/
unsigned char *co_cell2arg; /* Maps cell vars which are arguments. */
PyObject *co_filename; /* unicode (.py 文件名) */
PyObject *co_name; /* unicode (函数名/类名/模块名) */
PyObject *co_lnotab; /* string (encoding addr<->lineno mapping
字节码指令与 .py 行号的对应关系) */
void *co_zombieframe; /* for optimization only (see frameobject.c) */
PyObject *co_weakreflist; /* to support weakrefs to code objects */

/* Scratch space for extra data relating to the code object. */
void *co_extra;
} PyCodeObject;

需要注意的是,co_lnotab 记录行号,在实际中是记录增量值。

生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> src = open('demo.py').read()
>>> co = compile(src,'demo.py','exec')
>>> co
<code object <module> at 0x000002942853DD20, file "demo.py", line 1>
>>> type(co)
<class 'code'>
>>> co.co_consts
(<code object A , file "demo.py", line 1>, 'A', <code object fun, file "demo.py", line 5>, 'fun', None)
>>> cls_A = co.co_consts[0]
>>> type(cls_A)
<class 'code'>
>>> co.co_names
('A', 'fun', 'a')
>>> co.co_filename
'demo.py'
>>> cls_A.co_filename
'demo.py'

可见,PyObjectObject 通过 co_consts 字段,实现了嵌套。

字节码

1
2
3
4
5
6
// opcode.h
#define POP_TOP 1
#define ROT_TWO 2
...
#define HAVE_ARGUMENT 90
#define HAS_ARG(op) ((op) >= HAVE_ARGUMENT)

opcode.h中定义了所有的字节码指令,判断是否需要参数是根据HAVE_ARGUMENT宏来简单判断

1
2
3
4
5
6
7
8
9
10
11
>>> cls_A
<code object A , file "demo.py", line 1>
>>> import dis
>>> dis.dis(cls_A)
1 0 LOAD_NAME 0 (__name__)
2 STORE_NAME 1 (__module__)
4 LOAD_CONST 0 ('A')
6 STORE_NAME 2 (__qualname__)

2 8 LOAD_CONST 1 (None)
10 RETURN_VALUE

dis 工具能很好的解析字节码:

  • 第一列:字节码对应源码中的行号
  • 第二列:当前字节码指令在 co_code 中的偏移位置
  • 第三列:当前字节码指令
  • 第四列:oparg,指令参数
  • 最后一列:当前字节码指令的参数实际内容

栈帧

x86运行时栈帧

Python 解释器,本身拥有一套运行时的栈帧。但要执行源代码中的函数调用,却不是通过系统栈帧实现。在前文介绍 PyObject 时也发现,所有的 Object 都是存储在堆中。PyCodeObject 包含了运行时需要的信息,而其本身也存储在堆中。

因此,Python 在初始化后,会创建一个执行环境。发生函数调用时,会再次创建一个执行环境,并加载新的 PyCodeObject。这个执行环境,就是 PyFrameObject。关于函数调用的内容,会在 Pythn函数机制 中讲到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Stack frames 快速的创建和释放,下面是一些加速手段:

1. Hold a single "zombie" frame on each code object.

In zombie mode, 不持有 对象引用,但以下字段依然有效:
* ob_type, ob_size, f_code, f_valuestack;

* f_locals, f_trace,
f_exc_type, f_exc_value, f_exc_traceback are NULL;

* f_localsplus does not require re-allocation and
the local variables in f_localsplus are NULL.

2. 共享池技术 free list,池中的 FrameObject,只有以下字段有效:
ob_type == &Frametype
f_back next item on free list, or NULL
f_stacksize size of value stack
ob_size size of localsplus
Note that the value and block stacks are preserved.
不同于整数对象池,此处的 frame object 是具有内存空间的。
PyFrame_MAXFREELIST(200) 限制了最大 free_list 数目
*/

共享池真是无处不在…

PyFrameObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// frameobject.h.11
typedef struct {
int b_type; /* what kind of block this is */
int b_handler; /* where to jump to find handler */
int b_level; /* value stack level to pop to */
} PyTryBlock;

typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* previous frame, or NULL */
PyCodeObject *f_code; /* PyCodeObject 对象 */

PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
PyObject *f_globals; /* global symbol table (PyDictObject) */
PyObject *f_locals; /* local symbol table (any mapping) */

PyObject **f_valuestack; /* 运行时栈 栈底 */
PyObject **f_stacktop; /* 运行时栈 栈顶 */

PyObject *f_trace; /* Trace function */

/* 用于 generator 交换错误信息 */
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
/* Borrowed reference to a generator, or NULL */
PyObject *f_gen;

int f_lasti; /* 上一条字节码指令在 f_code 中的偏移位置 */
int f_lineno; /* 当前字节码,对应源代码行号
通过 PyFrame_GetLineNumber() 调用*/
int f_iblock; /* index in f_blockstack */
char f_executing; /* whether the frame is still executing */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* block 堆栈 */
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
} PyFrameObject;

f_back 表明 FrameObject 被组成链式结构,可以回溯,形成类似栈帧一样的结构。

PyFrame_New

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
PyFrameObject *
PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
PyObject *locals)
{
PyFrameObject *back = tstate->frame;
PyFrameObject *f;
PyObject *builtins;
Py_ssize_t i;

// 存在函数调用
if (back == NULL || back->f_globals != globals) {
// 获取 builtins (略)
}
else {
/* share the globals、 builtins.
Save a lookup and a call. */
builtins = back->f_builtins;
assert(builtins != NULL);
Py_INCREF(builtins);
}
// 尝试利用 zombieframe
if (code->co_zombieframe != NULL) {
f = code->co_zombieframe;
code->co_zombieframe = NULL;
_Py_NewReference((PyObject *)f);
assert(f->f_code == code);
}
else {
Py_ssize_t extras, ncells, nfrees;
ncells = PyTuple_GET_SIZE(code->co_cellvars); // 闭包:嵌套函数,使用了的变量
nfrees = PyTuple_GET_SIZE(code->co_freevars); // 使用了的外部作用域变量。

// Frame 所需动态内存大小
extras = code->co_stacksize + \ // 运行,栈空间(系统级)
code->co_nlocals + \ // 局部变量个数
ncells + \
nfrees;

if (free_list == NULL) {
f = PyObject_GC_NewVar(PyFrameObject, &PyFrame_Type, extras);
}
else {
assert(numfree > 0);
// 熟悉的套路, 链表,而不是数组
--numfree;
f = free_list;
free_list = free_list->f_back;
if (Py_SIZE(f) < extras) {
// 触发 resize 调整大小
PyFrameObject *new_f = PyObject_GC_Resize(PyFrameObject,
f, extras);
f = new_f;
}
_Py_NewReference((PyObject *)f);
}
// 封装属性
f->f_code = code;
// 此处,计算初始化时的栈顶
extras = code->co_nlocals + ncells + nfrees;
f->f_valuestack = f->f_localsplus + extras; // 栈底
for (i=0; i<extras; i++)
f->f_localsplus[i] = NULL;
f->f_locals = NULL;
f->f_trace = NULL;
f->f_exc_type = f->f_exc_value = f->f_exc_traceback = NULL;
}
//封装属性
f->f_stacktop = f->f_valuestack; // 初始化的 Frame,栈顶==栈底
f->f_builtins = builtins;
Py_XINCREF(back);
f->f_back = back;
Py_INCREF(code);
Py_INCREF(globals);
f->f_globals = globals;
// 处理 f->f_locals (略)
f->f_lasti = -1;
f->f_lineno = code->co_firstlineno;
f->f_iblock = 0;
f->f_executing = 0;
f->f_gen = NULL;

_PyObject_GC_TRACK(f);
return f;
}

创建 FrameObject 时,多创建了一部分作为运行时的栈空间。具体参考代码和下图:
新创建的FrameObject对象内存占用
Frame-extra

获取FrameObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# import sys; sys._getframe()
import inspect
def f():
frame = inspect.currentframe()
print(f"Current fun: {frame.f_code.co_name}")
caller = frame.f_back
print(f"Caller fun: {caller.f_code.co_name}")
print(f"Caller's local: {caller.f_locals}")
print(f"Caller's global: {caller.f_globals.keys()}")

def c():
l = 1
m = 2
f()

def show():
c()

结果显示,在被调用者中,完全可以通过 frame 链,获取到调用者的相关信息:

1
2
3
4
5
6
show()
Current fun: f
Caller fun: c
Caller fun: c
Caller's local: {'m': 2, 'l': 1}
Caller's global: dict_keys([..., 'inspect', 'f', 'c', 'show'])

作用域

Python 具有静态作用域,支持嵌套作用域,名字访问时按照LEGB规则访问属性。

Python 自身定义了一个 builtin 作用域,module 对应一个全局作用域 global,函数对应 local 作用域。这三个组成了LGB顺序查找模式。E 为 encloseing 的缩写,代表直接外围作用域,适用于函数闭包。

用名字访问时,用户代码的终点是 module,而 module 是在 import 编译完成时就已经确定了,所以永远不可能访问到其他 module 的相同名字,不会越界。

因为最内嵌套作用域的原因,决定 Python 行为的更多是代码出现的位置,而非执行的时间。

PyEval_EvalFrameEx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ceval.c.750
PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
PyThreadState *tstate = PyThreadState_GET();
return tstate->interp->eval_frame(f, throwflag);
}

// pystate.c.70
PyInterpreterState * PyInterpreterState_New(void)
{
...
interp->eval_frame = _PyEval_EvalFrameDefault;
...
}

// ceval.c.757
PyObject *
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
int opcode; /* Current opcode */
int oparg; /* Current opcode argument, if any */
enum why_code why; /* Reason for block stack unwind */

PyThreadState *tstate = PyThreadState_GET();
...
tstate->frame = f; // 设置线程状态中的栈帧对象
co = f->f_code;
names = co->co_names;
consts = co->co_consts;
fastlocals = f->f_localsplus;
freevars = f->f_localsplus + co->co_nlocals;
first_instr = (_Py_CODEUNIT *) PyBytes_AS_STRING(co->co_code);
next_instr = first_instr;
stack_pointer = f->f_stacktop;
f->f_stacktop = NULL; /* remains NULL unless yield suspends frame */
...
// ceval.c.1144
for (;;) {
// ceval.c.1267
switch (opcode) {
TARGET(LOAD_FAST) { ... }
TARGET(LOAD_CONST) { ... }
...
}
}
}

PEP 523中引入了 _PyEval_EvalFrameDefault,具体的逻辑都在其中。在 for 循环中,不断遍历字节码序列,然后 switch 执行。遍历过程中:

  • first_instr,始终指向字节码开始位置
  • next_inster,始终指向下一条待执行的位置(因参数影响,位置不固定)
  • frame.f_lasti,始终指向已经执行的上一条的位置

通过判断 why/why_not 字段,决定循环的状态。

1
2
3
4
5
6
7
8
9
10
/* Status code for main loop (reason for stack unwind) */
enum why_code {
WHY_NOT = 0x0001, /* No error */
WHY_EXCEPTION = 0x0002, /* Exception occurred */
WHY_RETURN = 0x0008, /* 'return' statement */
WHY_BREAK = 0x0010, /* 'break' statement */
WHY_CONTINUE = 0x0020, /* 'continue' statement */
WHY_YIELD = 0x0040, /* 'yield' operator */
WHY_SILENCED = 0x0080 /* Exception silenced by 'with' */
};

Code 与 Frame

CodeObject:

  • 每个命令空间都对应一个,可以嵌套
  • 存储着实际的变量值,变量名等等

FrameObject:

  • 通过属性 f_code,跟 CodeObject 进行关联
  • 维护这运行时的栈帧,f_lineno 等信息

先有鸡还是先有蛋?Python 环境初始化之后,通过run_file(fp, filename, &cf),进入到用户代码执行阶段。通过run_file调用链可以看出,最终运行的是_PyEval_EvalCodeWithName

1
2
3
4
5
6
7
8
9
// ceval.c
static PyObject *
_PyEval_EvalCodeWithName() {
...
f = PyFrame_New(tstate, co, globals, locals);
retval = PyEval_EvalFrameEx(f,0);
...
return retval;
}

很明显,先有 CodeObject,再有 FrameObject。在EvalFrame中,又会取出 f_code,执行用户代码。

1
2
3
4
5
static PyObject*
_PyFunction_FastCall() {
f = PyFrame_New(tstate, co, globals, NULL);
result = PyEval_EvalFrameEx(f,0);
}

在用户代码中,出现过程调用时,同样会再次创建 Frame。谁让 Frame 是维护的 Python 栈帧环境呢~

其实回想一下,CodeObject 是在编译阶段就已经创建,此时根本没有运行环境之说,肯定是先有鸡!