12月28, 2020

63. Python 是如何实现作用域的 (3)

注:之前打算直接研究 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 对变量作用域的一些 “精确定义”:

  1. local、函数参数、由 import 导入的变量都是 local 变量;
  2. 变量不能同时为 global 和 nonlocal;
  3. global 和 local 互斥,一个变量一旦属于一个集合,就 discard 另一个;
  4. nonlocal 只允许在local 域,或者说不能在最顶层的 module 中设置 nonlocal 变量;
  5. nonlocal 必须与一个 local 域变量绑定,不允许 “野” nonlocal 变量;
  6. 嵌套块的变量作用域,如果子块代码块中引用父级代码块的变量,那么除非该变量是 global 则在子块中依然为 global,否则该变量为 free(nonlocal)变量;

这些规则在递归下依然起作用。

本文链接:http://www.thinkinpython.com/post/deep_python_vm_63.html

-- EOF --