注:之前打算直接研究 Python 的命名空间实现,但后来发现目前局限在具体命名空间内的作用域问题更合适,对研究主题稍作调整。
Python 首先将 Python 源码转换为 AST,然后在 run_mod -> PyAST_CompileObject 中将 AST 逐步转换为最终的 CodeObject。
PyCodeObject *
PyAST_CompileObject(mod_ty mod, PyObject *filename, PyCompilerFlags *flags,
int optimize, PyArena *arena)
{
struct compiler c;
PyCodeObject *co = NULL;
// ··· ···
// 生成 symbol table 对象
c.c_st = PySymtable_BuildObject(mod, filename, c.c_future);
// ··· ···
// 转换为 CodeObject
co = compiler_mod(&c, mod);
// ··· ···
}
已经删减掉一些无关代码,该函数主要完成了:
AST -> Symbol table -> CodeObject
的转换。 前 2 节已经将 Symbol table 的生成的主要过程搞清楚了,在这个过程中 Python 完成了一些检查工作,并且构建了一系列 Symbol Table Entries,在这些 Entries 中实际上就包含了各自 scope 中所有名称的类型、作用域信息。
前面已经研究的内容已经足够实现简单的作用域,但这还不足以支撑 Python 灵活的作用域控制策略。在以 scope 为边界,整理好各自作用域内的名称后,还需要解决如:
- 局部变量对全局变量的遮蔽、
- 闭包变量的支持(nonlocal 关键字、
- 在子代码块中创建、使用全局变量(global 关键字)等等问题,
- 当然这其中还有 “类” 这样的高级议题,让我们暂时搁置它。
显然只在一次 AST 的扫描中构建的名称信息是不够的,需要更进一步的处理。
这些更进一步的问题,将由 PySymtable_BuildObject 函数尾部调用的 symtable_analyze 函数处理。
二次检查 Symbol table
struct symtable *
PySymtable_BuildObject(mod_ty mod, PyObject *filename, PyFutureFeatures *future)
{
// ··· ···
// 从最顶层代码开始,逐层构建 Symbol table
/* 第二轮调整 */
if (symtable_analyze(st))
// ··· ···
}
Python 源码的顶层是一个 “Module” 模块,所有其他代码都在这个 “Module” 内,逐层分解为各层 block,或者说 scope。第一轮扫描构建的名称信息,是以代码块为单位执行的,各代码块之间的影响在第二次扫描中解决,甚至这些代码块之间还会存在一些冲突,抛出一些异常。
symtable_analyze 的工作实际上主要是由 analyze_block 完成的,从名称也看的出来,分析的目标是 block,正好解决前面提到的各 block 之间的联系问题。analyze_block 会被递归调用到各级的 block(递归处理 ste->ste_children),各 block 通过 analyze_block 函数的指针参数向上一级传递自己的名称信息。
具体实现可以重点参考:
- Python/symtable.c:742 analyze_block 函数
- 和 Python/symtable.c:502 analyze_name 函数
在 analyze_name 函数中可以清晰的看到 Python 对变量作用域的一些 “精确定义”:
- local、函数参数、由 import 导入的变量都是 local 变量;
- 变量不能同时为 global 和 nonlocal;
- global 和 local 互斥,一个变量一旦属于一个集合,就 discard 另一个;
- nonlocal 只允许在local 域,或者说不能在最顶层的 module 中设置 nonlocal 变量;
- nonlocal 必须与一个 local 域变量绑定,不允许 “野” nonlocal 变量;
- 嵌套块的变量作用域,如果子块代码块中引用父级代码块的变量,那么除非该变量是 global 则在子块中依然为 global,否则该变量为 free(nonlocal)变量;
这些规则在递归下依然起作用。