登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python symtable 怎样区分自由变量与全局变量

来源:17golang原创

时间:2026-11-10 12:54:58 248浏览 收藏

当 Python 代码出现多层嵌套函数时,单看变量名很容易把“从外层闭包拿到的变量”和“从模块命名空间读取的变量”混在一起。symtable 的作用正是查看编译器为源代码建立的符号表:先找到目标函数对应的 SymbolTable,再用 Symbol 的标志位判断名称属于自由变量还是全局变量。

官方文档:https://docs.python.org/3/library/symtable.html

判断一个名称是否为自由变量,优先看目标函数表中该名称的 is_free();判断它是否按全局名称解析,使用 is_global(),还要用 is_declared_global() 区分是否明确写过 global。不要只凭名称是否出现在外层函数里猜结论。

先把两类变量放回作用域关系

可以把嵌套代码看成三个静态边界:模块、outer 外层函数和 inner 内层函数。内层函数引用外层函数赋值的名称时,这个名称对内层来说是自由变量;内层引用模块级名称时,它属于全局解析路径。两者都可能“不在 inner 自己的局部赋值里”,但来源不同,不能用同一个判断替代。

Python symtable 模块外层函数内层函数与自由变量全局变量的作用域关系说明图
图1:作用域边界说明图,展示 inner 查看的变量为什么分成自由变量和全局变量;这不是运行截图。

下面的示例只作为静态分析输入。enclosed_name 在 outer 中绑定,module_name 在模块层绑定;inner 同时引用两者,但没有在自身赋值。

import symtable

module_name = "from module"

def outer():
    # 这个名称由 outer 绑定,inner 读取时属于自由变量。
    enclosed_name = "from outer"

    def inner():
        # inner 不绑定这两个名称,只建立对外层和模块层的引用。
        return enclosed_name, module_name

    return inner

# 这里读取的是源代码的符号表,不执行 outer 或 inner。
source = '''
module_name = "from module"
def outer():
    enclosed_name = "from outer"
    def inner():
        return enclosed_name, module_name
    return inner
'''
table = symtable.symtable(source, "scope_demo.py", "exec")

先取得 inner 对应的 SymbolTable

symtable.symtable() 返回顶层符号表。顶层表代表模块,嵌套函数则通过 get_children() 暴露为子表。工程代码里不要假设“第一个子表一定是目标函数”,更稳妥的做法是按 get_name() 和 get_type() 找到目标,再继续向下查找。

def find_child(table, name):
    # 按名称查找当前代码块的直接子表,避免依赖列表位置。
    for child in table.get_children():
        if child.get_name() == name:
            return child
    raise LookupError(f"找不到符号表: {name}")

module_table = symtable.symtable(source, "scope_demo.py", "exec")
outer_table = find_child(module_table, "outer")
inner_table = find_child(outer_table, "inner")

# inner_table 现在就是要分析的目标函数表。
print(inner_table.get_type())

这种查找方式的好处是把“代码块定位”和“名称归类”分开。前者由 SymbolTable 负责,后者交给 Symbol。如果目标代码包含类、类型别名或其他新的符号表类型,也不要把返回值硬编码成普通函数字符串;官方文档建议使用 SymbolTableType 成员理解类型。

用 lookup() 逐个区分自由变量与全局变量

拿到 inner_table 后,关键动作是 lookup(name)。返回的 Symbol 同时提供多个布尔标志:is_free() 表示名称在当前块被引用、但没有在当前块赋值;is_global() 表示它按全局名称处理;is_declared_global() 则只回答是否出现过明确的 global 声明。

def classify(table, name):
    # lookup 失败时应保留名称上下文,便于静态分析工具报错。
    symbol = table.lookup(name)
    return {
        "name": symbol.get_name(),
        "free": symbol.is_free(),
        "global": symbol.is_global(),
        "declared_global": symbol.is_declared_global(),
        "referenced": symbol.is_referenced(),
        "assigned": symbol.is_assigned(),
    }

enclosed_info = classify(inner_table, "enclosed_name")
module_info = classify(inner_table, "module_name")

在这个例子里,enclosed_name 的判断重点是 free=True;module_name 的判断重点是 global=True。而 declared_global 与 global 不是同义词:一个名称可以按隐式全局规则解析,却没有在函数体里写 global module_name。

如果代码显式声明全局变量,静态结果应单独记录这一层信息:

source_with_global = '''
counter = 0
def read_counter():
    # 这个声明明确告诉编译器名称来自模块作用域。
    global counter
    return counter
'''

table = symtable.symtable(source_with_global, "global_demo.py", "exec")
reader = find_child(table, "read_counter")
counter = reader.lookup("counter")

# is_global 表示全局归类,is_declared_global 表示语法上明确声明。
print(counter.is_global(), counter.is_declared_global())

函数级集合适合做快速摘要

逐名称检查适合展示原因,批量分析时则可以先看函数表提供的集合。函数类型的符号表有 get_frees()、get_globals() 和 get_nonlocals(),分别返回自由变量、全局变量以及显式 nonlocal 名称的元组。

Python symtable lookup Symbol 标志位与函数级自由变量全局变量集合的关系图
图2:判定关系结构图,展示 Symbol 的单项标志与函数级集合之间的对应关系;这不是运行截图。
# 这些集合适合先做概览,再回到 lookup() 解释某个名称。
summary = {
    "frees": inner_table.get_frees(),
    "globals": inner_table.get_globals(),
    "nonlocals": inner_table.get_nonlocals(),
}

# 对阅读工具来说,排序只影响展示,不改变符号表的语义。
for key, names in summary.items():
    print(key, sorted(names))

get_frees() 解决的是“这个函数依赖哪些外层闭包名称”,get_globals() 解决的是“这个函数把哪些名称交给全局命名空间解析”。要追问某个名称为什么被归类,仍应回到 lookup(),把 is_free()、is_global()、is_declared_global() 和赋值/引用状态一起展示。

几个容易误判的边界

观察到的情况更准确的解释建议读取的 API
当前函数引用外层函数的绑定对当前函数通常是自由变量,属于闭包解析的一部分lookup().is_free()、get_frees()
当前函数引用模块层名称按全局名称解析,但不代表一定写过 globallookup().is_global()
函数体出现 global 声明全局归类之外,还能确认语法上的显式声明is_declared_global()、get_globals()
函数体出现 nonlocal 声明名称被明确指向最近的非模块外层作用域is_nonlocal()、get_nonlocals()

还要留意类作用域不是普通的函数闭包边界。官方文档专门说明,方法视角下的类作用域名称可能通过 is_free_class() 表示;因此分析类体和方法时,不要把“外层有同名赋值”直接套进普通嵌套函数规则。

把 symtable 放在静态分析层

symtable 读取的是编译器在生成字节码前建立的符号表,它回答“名称在代码结构中怎样归类”,不回答某次运行中变量当前保存了什么值,也不替代执行期的异常和数据流分析。规模较大的代码扫描器可以采用两层结构:先用 get_children() 建立代码块树,再用 lookup() 和函数级集合生成名称报告。

这样做的取舍很明确:静态结果稳定、无需执行不可信代码,适合做作用域解释、代码导航和规则提示;代价是它不能证明运行时分支一定走到某个引用,也不能替代对动态属性、eval 或导入副作用的运行时判断。

相关问题

  • 自由变量是不是一定来自直接上一层函数?不一定,关键是它来自当前函数之外、且参与闭包解析的外层绑定,不能只按物理层级猜测。
  • is_global() 为真是否等于写了 global?不是。要确认是否明确声明,还要看 is_declared_global()。
  • 为什么不直接用 locals() 和 globals()?它们面向运行时命名空间;symtable 面向源代码的静态作用域分类,两者解决的问题不同。
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>