Python 代码编译运行过程(2):Python 虚拟机
简单总结一下 Python 代码编译运行过程
前言
阅读 Pytorch 源码时涉及到 Python 代码编译执行相关的内容,为了便于理解,简单学习了 Inside The Python Virtual Machine 的部分内容,本文内容主要来自于此,相关细节请参考原文。
Code Objects
Python 程序由代码块构成,交互模式下的每个命令、脚本文件都是代码块,当 Python 编译代码块时,都会生成 code object:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def fizzbuzz(n):
if n % 3 == 0 and n % 5 == 0:
return 'FizzBuzz'
elif n % 3 == 0:
return 'Fizz'
elif n % 5 == 0:
return 'Buzz'
else:
return str(n)
# 打印代码对象内容
for attr in dir(fizzbuzz.__code__):
if attr.startswith('co_'):
print(f"{attr}:\t{getattr(fizzbuzz.__code__, attr)}")
# 反编译得到的字节码指令
import dis
dis.dis(fizzbuzz)
上面代码的打印结果是:
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
# ===================代码对象内容
co_argcount: 1
co_cellvars: ()
co_code: b'|\x00d\x01\x16\x00d\x02k\x02r\x1c|\x00d\x03\x16\x00d\x02k\x02r\x1cd\x04S\x00|\x00d\x01\x16\x00d\x02k\x02r,d\x05S\x00|\x00d\x03\x16\x00d\x02k\x02r<d\x06S\x00t\x00|\x00\x83\x01S\x00d\x00S\x00'
co_consts: (None, 3, 0, 5, 'FizzBuzz', 'Fizz', 'Buzz')
co_filename: <ipython-input-1-2a4d59ff6c50>
co_firstlineno: 1
co_flags: 67
co_freevars: ()
co_kwonlyargcount: 0
co_lnotab: b'\x00\x01\x18\x01\x04\x01\x0c\x01\x04\x01\x0c\x01\x04\x02'
co_name: fizzbuzz
co_names: ('str',)
co_nlocals: 1
co_posonlyargcount: 0
co_stacksize: 2
co_varnames: ('n',)
# ===================字节码指令
2 0 LOAD_FAST 0 (n)
2 LOAD_CONST 1 (3)
4 BINARY_MODULO
6 LOAD_CONST 2 (0)
8 COMPARE_OP 2 (==)
10 POP_JUMP_IF_FALSE 28
12 LOAD_FAST 0 (n)
14 LOAD_CONST 3 (5)
16 BINARY_MODULO
18 LOAD_CONST 2 (0)
20 COMPARE_OP 2 (==)
22 POP_JUMP_IF_FALSE 28
3 24 LOAD_CONST 4 ('FizzBuzz')
26 RETURN_VALUE
4 >> 28 LOAD_FAST 0 (n)
30 LOAD_CONST 1 (3)
32 BINARY_MODULO
34 LOAD_CONST 2 (0)
36 COMPARE_OP 2 (==)
38 POP_JUMP_IF_FALSE 44
5 40 LOAD_CONST 5 ('Fizz')
42 RETURN_VALUE
6 >> 44 LOAD_FAST 0 (n)
46 LOAD_CONST 3 (5)
48 BINARY_MODULO
50 LOAD_CONST 2 (0)
52 COMPARE_OP 2 (==)
54 POP_JUMP_IF_FALSE 60
7 56 LOAD_CONST 6 ('Buzz')
58 RETURN_VALUE
9 >> 60 LOAD_GLOBAL 0 (str)
62 LOAD_FAST 0 (n)
64 CALL_FUNCTION 1
66 RETURN_VALUE
68 LOAD_CONST 0 (None)
70 RETURN_VALUE
- co_code:这些字节码指令序列中的每一个都由一个操作码(opcode)和一个参数(opatg)组成。 例如,
co.co_code[0]
返回指令的第一个字节124
,该字节对应于 python 的LOAD_FAST
操作码。使用dis.dis(co_code)即可以得到字节码指令。 - co_consts:常量列表,字节码里面的 LOAD_CONST 1(3)即是加载常量列表索引为1的值,即3。
- co_firstlineno:代码对象第一行行号。
- co_flag:该字段指示代码对象的种类。 例如,当代码对象是协程的对象时,该标志设置为
0x0080
。 还有其他标志,例如CO_NESTED
指示一个代码对象是否嵌套在另一个代码块内,CO_VARARGS
指示一个代码块是否具有变量自变量,等等。 这些标志会影响字节码执行期间求值循环的行为。
Frames Objects
code object 包含可执行的字节代码,但缺少执行此类代码所需的上下文信息。可以将 frame object 视为执行代码对象的容器,并且它引用了某些代码对象执行期间所需的数据和值
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
typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* previous frame, or NULL
该字段是对在当前代码对象之前执行的代码对象所在 frame 的引用。
给定一组 frame 对象,这些 frame 的 f_back 字段一起组成一个 frame 栈结构,这些 frame 一直返回到初始 frame。
然后,初始 frame 在 f_back 字段的值为 NULL 。 这种隐式 frame 堆栈形成了我们称为调用堆栈的 frame。*/
PyCodeObject *f_code; /* code segment
该字段是对代码对象的引用。
此代码对象包含在该 frame 的上下文中执行的字节码。*/
PyObject *f_builtins; /* builtin symbol table (PyDictObject)
这是对内置名称空间的引用。 该名称空间包含诸如 print,enumerate 等名称及其对应的值。*/
PyObject *f_globals; /* global symbol table (PyDictObject) 这是对代码对象的全局名称空间的引用。*/
PyObject *f_locals; /* local symbol table (any mapping)
这是对代码对象的局部名称空间的引用。
如前所述,这些名称已在函数范围内定义。
当我们讨论 f_localplus 字段时,我们将看到 python 在使用局部定义的名称时所做的优化。*/
PyObject **f_valuestack; /* points after the last local
这是对 frame 求值堆栈的引用。
回想一下,Python 虚拟机是基于堆栈的虚拟机,因此在对字节码进行求值期间,将从堆栈的顶部读取值,并将字节码求值的结果存储在堆栈的顶部。
该字段是在代码对象执行期间使用的堆栈。
frame 代码对象的堆栈大小提供了此数据结构可以扩展到的最大深度。*/
/* Next free slot in f_valuestack. Frame creation sets to f_valuestack.
Frame evaluation usually NULLs it, but a frame that yields sets it
to the current stack top. */
PyObject **f_stacktop;
PyObject *f_trace; /* Trace function */
char f_trace_lines; /* Emit per-line trace events? */
char f_trace_opcodes; /* Emit per-opcode trace events? */
/* Borrowed reference to a generator, or NULL */
PyObject *f_gen;
int f_lasti; /* Last instruction if called */
/* Call PyFrame_GetLineNumber() instead of reading this field
directly. As of 2.3 f_lineno is only valid when tracing is
active (i.e. when f_trace is set). At other times we use
PyCode_Addr2Line to calculate the line from the current
bytecode index. */
int f_lineno; /* Current line number */
int f_iblock; /* index in f_blockstack */
char f_executing; /* whether the frame is still executing */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
} PyFrameObject;
指令执行
frame 对象执行情况可以参考下图
frame 中的指令最终由 _PyEval_EvalFrameDefault 函数执行,函数内部主要是 switch-case 逻辑,执行对应的指令
1
2
3
4
5
6
...
switch (opcode) {
case TARGET(LOAD_CONST): { /* 加载常量 */
...
}
...
This post is licensed under CC BY 4.0 by the author.