# 8.5 符号表

在编译代码之前，`PySymtable_BuildObject()` API 会创建出一个**符号表**(symbol table)。

符号表的目的是提供命名空间、全局和局部的列表，供编译器用于引用和解析作用域。

### 相关源文件

下面是与符号表相关的源文件：

| 文件                 | 用途               |
| ------------------ | ---------------- |
| Python/symtable.c  | 符号表实现            |
| Include/symtable.h | 符号表 API 定义和类型定义  |
| Lib/symtable.py    | `symtable` 标准库模块 |

### 符号表数据结构

`symtable` 结构应该是编译器的一个 `symtable` 实例，因此命名空间变得至关重要。

例如，如果你在一个类中创建了一个叫做 `resolve_names()` 的方法，并在另一个类中也声明了一个同名的方法，那么你需要确定哪个方法要在模块内被调用。

`symtable` 服务于此目的，同时确保在狭窄范围内声明的变量不会自动成为全局变量。

符号表结构(`symtable`)具有以下字段：

| 字段               | 类型                 | 用途                           |
| ---------------- | ------------------ | ---------------------------- |
| recursion\_depth | int                | 当前递归深度                       |
| recursion\_limit | int                | 引发 `RecursionError` 错误前的递归限制 |
| st\_blocks       | PyObject \* (dict) | AST 节点地址到符号表项的映射             |
| st\_cur          | \_symtable\_entry  | 当前符号表项                       |
| st\_filename     | PyObject \* (str)  | 正在编译的文件名                     |
| st\_future       | PyFutureFeatures   | 影响符号表的模块未来特性                 |
| st\_global       | PyObject \* (dict) | 对 `st_top` 中的符号引用            |
| st\_nblocks      | int                | 使用的块数                        |
| st\_private      | PyObject \* (str)  | 当前类的名字(可选)                   |
| st\_stack        | PyObject \* (list) | 命名空间信息栈                      |
| st\_top          | \_symtable\_entry  | 模块的符号表项                      |

### 使用 symtable 标准库模块

有些符号表 C API 通过标准库中的 `symtable` 模块在 Python 中公开出来。

使用另一个称为 `tabulate`(可在 PyPI 上获取)的模块，你可以创建一个脚本来打印符号表。

符号表可以嵌套，因此如果模块包含函数或类，则该模块将具有符号表。

创建一个名为 `symviz.py` 的脚本，并有一个递归 `show()` 函数：

```python
import tabulate
import symtable
code = """
def calc_pow(a, b):
return a ** b
a = 1
b = 2
c = calc_pow(a,b)
"""
_st = symtable.symtable(code, "example.py", "exec")
def show(table):
print("Symtable {0} ({1})".format(table.get_name(),
table.get_type()))
print(
    tabulate.tabulate(
        [
            (
                 symbol.get_name(),
                 symbol.is_global(),
                 symbol.is_local(),
                 symbol.get_namespaces(),
            )
            for symbol in table.get_symbols()
        ],
        headers=["name", "global", "local", "namespaces"],
        tablefmt="grid",
    )
)
if table.has_children():
    [show(child) for child in table.get_children()]


show(_st)
```

在命令行运行 `symviz.py` 以查看示例代码的符号表：

图片不翻译

### 符号表实现

符号表的实现在 `Python/symtable.c` 中，并且主接口是 `PySymtable_BuildObject()`。

与上一章所讲到的 AST 编译类似，`PySymtable_BuildObject()` 在 `mod_ty` 的可能类型(Module、Interactive、Expression 和 FunctionType)之间切换并访问其中的每个语句。

符号表递归地探索 AST(`mod_ty` 类型)的节点和分支，并将条目添加到 `symtable` 中：

```c
struct symtable *
PySymtable_BuildObject(mod_ty mod, PyObject *filename,
                       PyFutureFeatures *future)
{
    struct symtable *st = symtable_new();
    asdl_seq *seq;
    int i;
    PyThreadState *tstate;
    int recursion_limit = Py_GetRecursionLimit();
...
    st->st_top = st->st_cur;
    switch (mod->kind) {
    case Module_kind:
        seq = mod->v.Module.body;
        for (i = 0; i < asdl_seq_LEN(seq); i++)
            if (!symtable_visit_stmt(st,
                        (stmt_ty)asdl_seq_GET(seq, i)))
                goto error;
        break;
    case Expression_kind:
        ...
    case Interactive_kind:
        ...
    case FunctionType_kind:
        ...
    }
    ...
}
```

对于模块而言，`PySymtable_BuildObject()` 会循环遍历模块中的每个语句并调用 `Symtable_visit_stmt()`，这是一个巨大的 `switch` 语句，并且每个语句类型都有一个与之对应的 `case`(在 `Parser/Python.asdl` 中定义)。

每个语句类型都有相应的函数来解析符号。例如，函数定义(`FunctionDef_kind`)语句类型对以下操作具有特定的逻辑：

* 根据递归限制检查当前递归深度；
* 将函数的名称添加到符号表中，以便它可以作为函数对象被调用或传递；
* 解析符号表中的非字面量默认参数；
* 解析类型注解；
* 解析函数装饰器。

最后，`symtable_enter_block()` 访问带有函数内容的块，然后访问并解析参数和函数体。

{% hint style="info" %}
**重要**

如果你曾经想知道为什么 Python 的默认参数是可变的，原因就在 `symtable_visit_stmt()` 中。参数默认值是对 `symtable` 中变量的引用。

将任何值复制到不可变类型不需要做额外的工作。
{% endhint %}

以下是在 `symtable_visit_stmt()` 中为函数构建 `symtable` 相关步骤的C代码：

代码不翻译

一旦创建了结果符号表，它就会传递到编译器中。
