前面说到 CodeObject 的结构,其中忽略了一个重要的问题:CodeObject 中记录了 names、contants 之类的信息。根据我们使用 Python 的经验来说,不可能整个 Python 程序的所有名称和常量都在一起,那样可就乱了套了,Python 通过 “命名空间(namespace)” 化整为零管理这一大堆对象。显然,一个稍微复杂一点的 Python 程序不太可能只有一个 CodeObject 这么简单。Python 是如何一层层组织这些命名空间的呢?提前预告一下,这些命名空间与作用域的实现息息相关。
一切从 AST 说起
从 Python 源码开始,一直到最终 VM 执行代码,可谓是一波三折,Python 在背后做了大量的工作,支撑这 Python 极高的易用性。命名空间出现的具体时间在逻辑上很难把握,因为从 AST 开始命名空间就已经呼之欲出了,缺的只是一个明确的准则。但在具体的实现上,可以大概确定在某个位置,命名空间就算是确定了。
让我们再回到 PyRun_FileExFlags 函数,希望你还记得它,只要在调试模式下,让 Python 执行一个简单的 Python 代码 test.py:
a = 1
def func():
b = 1
func()
这一次,我们在代码中定义了一个函数 func,我们确定函数里会有一个新的命名空间,且看 Python 是如何实现它的。在调试模式下设置断点到 PyRun_FileExFlags,Python 会在这里对 test.py 进行解析、编译和执行。
PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
PyObject *locals, int closeit, PyCompilerFlags *flags)
{
// 生成 AST
mod = PyParser_ASTFromFileObject(fp, filename, NULL, start, 0, 0,
flags, NULL, arena);
// ··· ···
// 运行 AST
ret = run_mod(mod, filename, globals, locals, flags, arena);
// ··· ···
}
创建 AST 的时候,我认为已经有命名空间的意思在里面,但还不够清晰,我们需要继续跟进到 run_mod 中。
// Python/pythonrun.c:1141
static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
PyCompilerFlags *flags, PyArena *arena)
{
// ··· ···
// 编译 AST,还需要继续深入,才能找到的 namespace 的创世时刻
co = PyAST_CompileObject(mod, filename, flags, -1, arena);
// ··· ···
// 在这里执行代码
v = run_eval_code_obj(co, globals, locals);
Py_DECREF(co);
return v;
}
// Python/compile.c:L312
PyCodeObject *
PyAST_CompileObject(mod_ty mod, PyObject *filename, PyCompilerFlags *flags,
int optimize, PyArena *arena)
{
// ··· ··
// 构建符号表
c.c_st = PySymtable_BuildObject(mod, filename, c.c_future);
// ··· ···
// 编译为 CodeObject
co = compiler_mod(&c, mod);
// ··· ···
}
一切都发生在最后的 PySymtable_BuildObject 函数中,有兴趣的朋友可以单步跟进这个函数。从该函数开始,有可能会进入递归调用,直到所有的代码层次都被解析完毕。
Scope(作用域或者代码块)
为了方便,首先将 test.py 源码的 AST 也一起给出:
对于 Python 执行 Python 源码文件这种形式,首先会创建一个类型为 Module 的顶级 ste (Symbol Table Entry),对应 AST 的根节点。
在解析的过程中,Python 也记录当前的递归深度和当前的 Scope。
- 在执行 Python 源码文件时,整个文件被看作一个 Module,也就是最外一层,此时递归深度为 0;
- 每当深入一层,递归深度 + 1, 反之上升一层递归深度就 - 1;
- 对于某一层内所有的代码,它们都是 stmt(statement,语句)。
我用一张图将这些信息表达出来:
特别的:
- Scope 用红色标注;
- stmt 用绿色标注;
- 实际上每一个 stmt 还会继续向下递归,拆分为 Name、Constant 之类,但是这里并没有展示这些细节;
- 用蓝底灰字表示所处区域的递归深度。
Python 在进入一个新的 Scope 前,会调用 symtable_enter_block 函数,创建全新的 Scope,并且之后的所有解析都在这个 Scope 下进行,在退出 Scope 时执行 symtable_exit_block 函数,切换回上一层 Scope。
在 PySymtable_BuildObject 正式解析 AST 之前,先创建了一个顶级 Module scope,之后开始 深度优先 递归遍历 AST。在 Module 这一递归层内,所有语句都看作独立的 stmt,比如 a=1 和函数 func 的定义是完全对等的, 具体对 func 的解析则创建新 Scope, 并递归到函数正文中完成。当然,最后的 func() 也与之前的语句一样,都是 stmt。
什么时候会创建新的 Scope
源码中全局搜索 symtable_enter_block 的调用可知以下几种语句会进入新的 Scope:
- FunctionDef_kind,函数定义语句;
- ClassDef_kind,类定义语句;
- AsyncFunctionDef_kind,Async 函数定义语句;
- Lambda_kind, Lambda 定义语句;
- symtable_handle_comprehension函数,作用尚不清楚,待研究。
基本上可以支持我们对 Python 语言作用域的使用经验。
这似乎已经足够说清楚 Python 的命名空间的实现了,事实上远不止如此,后面研究 Frame 的时候还会有一些新的发现。