现在我们已经深入到 ByteCode 在 Python 的中表示:PyCodeObject,是的,Python 自身的字节码也是对象。PyCodeObject 中提供了运行 Python 程序所需要的一切信息,最终由 VM 接手处理。
准备工作
VM 是 Python 的本质,也是核心,其最关键的执行部分是一个非常非常长的大函数,夹杂着大量 goto、注释、宏定义和条件编译,最好可以在调试模式下,让 GDB 领路,这样痛感会小的多。
之前已经知道,Python 内部对执行源文件、交互模式做了差别处理,分别由不同的函数接手,它们最终必然是殊途同归,我们就只关注执行源文件这一种情况就好。所以在 GDB 调试 Python(当然,其他调试工具类似,但是我钟爱 GDB)的时候,我们让 Python 以这样的等效模式运行:
python test.py
test.py 里只有非常简单的一行代码:
print("hello world.")
另外,在 50 节中介绍了通过 GDB 插件,直接打印 Python 对象的方法,希望对你有用。
第一个断点可以设置到 pymain_run_file,也就是 Python 接手处理源文件的函数:
(gdb) b pymain_run_file
然后在 GDB 中运行:
(gdb) r test.py
预先设置好的断点,会让 Python 很快停到 pymain_run_file,之后依次深入这些函数,步步为营:
终于深挖到 run_eval_code_obj,名字就已经出卖了它:
肯定跟执行 CodeObject 有什么关系!
到了这里,就算离 VM 的核心很近了。下来依次剥开基层包装函数:
run_eval_code_obj->PyEval_EvalCode->_PyEval_EvalCodeWithName
Frame 对象
在 _PyEval_EvalCodeWithName 函数里,创建了一个一个新的 PyFrameObject 对象:
//Python/ceval.c:4053
PyObject *
_PyEval_EvalCodeWithName( /* so many args, let's ignore them. */ ){
// ··· ···
// 新创建的 PyFrameObject
f = _PyFrame_New_NoTrack(tstate, co, globals, locals);
// ··· ···
// 执行这个 frame
retval = PyEval_EvalFrameEx(f,0);
// ··· ···
}
还记得上一节我们想象的,VM 有一个工作场所,那里有它所需要的一切:
- 常量列表;
- 名称列表;
- 栈
- blabla
这一切就包含在 f 中了,接下来要发生的一切也都在 f 中,也就是 PyEval_EvalFrameEx(f,0) 中,在这两个函数之间,有一段漫长的准备工作,主要是按照 CodeObject 提供的信息构建 f。如果 CodeObject 是一个菜谱,那么 f 就是最终的那盘菜。
当一切就绪,就可以继续深入,很快就到最激动人心的地方了,我为找到这里激动了一个下午!
执行 Frame
只要继续下探 2 层就到了本次旅行的终点 _PyEval_EvalFrameDefault (Python/ceval.c:745)。这个函数单是它的体量就足够向我们证明它的地位——长达 3000 多行!
相信我,这就是 Python 的本质了。Frame 中已经构建好了执行字节码所需的大部分要素,经过一番准备,这个函数最核心的逻辑是这样:
for (;;){
opcode = _Py_OPCODE(*next_instr);
switch(opcode ){
case TARGET(NOP): { /* */};
case TARGET(LOAD_FAST): { /* */};
case TARGET(STORE_FAST): { /* */};
// etc.
}
}
一条一条的操作码 (OPCODE) 从 CodeObject 中取出交给核心 switch 执行,一条流水线就这样建立起来了!