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 自己的局部赋值里”,但来源不同,不能用同一个判断替代。

下面的示例只作为静态分析输入。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 名称的元组。

# 这些集合适合先做概览,再回到 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() |
| 当前函数引用模块层名称 | 按全局名称解析,但不代表一定写过 global | lookup().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面向源代码的静态作用域分类,两者解决的问题不同。
-
457 收藏
-
317 收藏
-
346 收藏
-
235 收藏
-
485 收藏
-
265 收藏
-
464 收藏
-
217 收藏
-
366 收藏
-
156 收藏
-
471 收藏
-
309 收藏
-
433 收藏
-
361 收藏
-
271 收藏
-
486 收藏
-
494 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习