对于 Python 是一门优雅的语言,这一点应该不会有太多人反对,随着我们对 CPython 实现的研究,不得不说 Python 的优雅一直渗透到它的底层,在 Python 的 C 实现这一层,几乎全部以 Python C API 实现,函数之间传递的是 Object *,Dict、Tuple、List 等对象,在 CPython 的 C 实现中更是随处可见。
如果将 Python 进行分层,最底层是基本的内存管理,之上就是 Python 强大的类型和面向对象系统,Python VM 甚至也是高度依赖于其自身的类型系统,在最顶层支撑了 Python 语言的用户接口。Python 语言实际上是对 Python 类型系统和 VM 的操作。
一切从 main 开始
所有 C 语言程序都必须有一个 main 函数,Python,更准确的说 CPython 也是一个程序,main 就是它运行的开端。找到这个开端,就算是理清 “头绪” 的第一步。
在 GDB 中设置 main 函数的断点,很容易找到 Python 的入口位置:
// Programs/python.c:16
int
main(int argc, char **argv)
{
return Py_BytesMain(argc, argv);
}
非常简单,可以看到进入 main 函数之后,只有一个简短的 Py_BytesMain 函数,实际上这个函数可不简单,Python 的一切都在这个函数里,如果在交互模式下,这个函数也不会像想象中一样迅速返回。如果一直追踪下去,会遇到下面这个比较长的函数:
// Modules/main.c:551
static void
pymain_run_python(int *exitcode)
{
// ··· ···
// some if-else conditions
else if (config->run_filename != NULL) {
// 执行 py 文件
*exitcode = pymain_run_file(config, &cf);
}
else {
// 交互模式
*exitcode = pymain_run_stdin(config, &cf);
}
// ··· ···
}
在这里 Python 的运行流程开始分道扬镳,执行文件,或者进入交互模式从标准输入读取指令,分别由不同的函数处理。Python 的初始化工作也大体结束,真正开门营业了。
如果留意源码中的 Object *,实际上此时 Python VM 还什么都没有干呢,但 Python 对象已经先跑起来了。Object 很早就存在于 Python 中,这也是在研究 VM 之前,破天荒的优先认识类型系统的原因。
py 文件的执行
Python 的交互模式可以认为是临时一行一行写代码,写一点执行一点。为了方便,我们现在先搞清楚 py 文件是怎么执行的就好了,也就是要让程序流运行到 pymain_run_file 里面去。方法也很简单,在 GDB 执行 run test.py 就可以了,这就相当于执行 python test.py。这个 test.py 是我临时写的一小段代码;
print("hello world")
"hello world" 是如此流行,我们也遵守这个习惯好了。
如果一切顺利,很快就会找到这里:
// Python/pythonrun.c:1039
PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
PyObject *locals, int closeit, PyCompilerFlags *flags)
{
// ··· ···
filename = PyUnicode_DecodeFSDefault(filename_str);
// ··· ···
// 从文件对象,构建 AST
mod = PyParser_ASTFromFileObject(fp, filename, NULL, start, 0, 0,
flags, NULL, arena);
// 执行该语法数
ret = run_mod(mod, filename, globals, locals, flags, arena);
// ··· ···
}
// Python/pythonrun.c:1132
static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
PyCompilerFlags *flags, PyArena *arena)
{
// ··· ···
// 编译,生成 codeobject,co 是它的缩写
co = PyAST_CompileObject(mod, filename, flags, -1, arena);
// ··· ···
// 执行 codeobject
v = run_eval_code_obj(co, globals, locals);
Py_DECREF(co);
return v;
}
到了这里,我们可以暂时先不继续深入跟进,可以看到在 run_eval_code_obj 返回的时候,test.py 已经正确执行,打印了 "hello wold"。
如果算上 PyRun_FileExFlags 之前某处打开 py 文件,那么 py 文件的运行至少经历了这些步骤:
- test.py 也就是源码文件,首先被打开并封装为 Python 的文件对象;
- 之后构建 AST,即抽象语法树,编译原理是另外一个话题了,有兴趣可以阅读编译原理经典著作龙书、虎书、鲸书;
- 再将 AST 转换为 Python 字节码,源码中已经可以看出,字节码保存在 codeobject 中;
- 最终执行该字节码,结束。
这样一个简单的 py 文件就执行完毕了。
文件操作、编译原理等问题已经超出我们研究的范围,另外在生成 codeobject 前还有一个优化过程,我们也暂时搁置这一议题,后面我们将会重点研究 codeobject,以及 Python VM 到底是如何响应字节码的指令,最终把 Python “运行” 起来的。