本节摘要:抽象语法树(AST)是源码去掉括号、分号等表面细节后剩下的纯结构表示,是编译器与一切现代元设施交换信息的通用货币。本节练习把嵌套表达式画成树、用 Python 标准库亲手验证树结构,并解释宏、静态分析、代码格式化工具为何都长在 AST 之上。
人读代码靠的是文本——缩进、括号、运算符优先级。编译器读代码靠的是树。一段 a + b * c 在它眼里不是七个字符,而是一棵树:加法节点在上,左边挂着变量 a,右边挂着一棵乘法子树,乘法子树下面才是 b 和 c。优先级不再靠规则推导——它已经长成了树的形状。
这棵树就是抽象语法树。所谓"抽象",指它丢掉了一切与语义无关的细节:空格、换行、括号、分号都不在树里。两个文本不同但语义相同的表达式(a+b 与 a + b)生成同一棵树。文本是给人看的皮肤,树才是编译器认的骨骼——这句话值得单独念一遍,因为 3.2 讲的"语法宏操作结构而非字符",操作的对象就是它。

多数语言把 AST 藏在编译器内部,Python 是少数把它整个放进标准库、人人可用的语言——这让它成为学 AST 的最佳沙盒。
import ast tree = ast.parse("discount = price * (1 - rate)") print(type(tree)) # 模块节点:整棵树的根 assign = tree.body[0] # 取第一条语句:赋值节点 print(type(assign).__name__) # Assign print(type(assign.value).__name__) # BinOp(二元运算) print(assign.value.op) # Mult(乘法)——括号已被吸收 print(ast.dump(assign.value.right, indent=8)) # 右操作数是一棵子树:BinOp(op=Sub, left=Constant(1), right=Name('rate'))
操作→结果:ast.parse 返回树根,body 是语句列表,assign.value 是表达式子树;dump 输出确认了减法子树的结构——与上图完全一致。解读:注意 (1 - rate) 的括号在树里无影无踪,优先级变成了"减法子树是乘法节点的孩子"。变式:把表达式换成 discount = (price * 1) - rate,树的形状立刻变成"乘法子树挂在减法节点下"——语义不同的代码长成了不同的树,静态分析工具查错误就是靠比对这些形状。
回头看 3.2 的结论"现代宏必须操作语法树",现在可以补全理由了。宏在编译期收到的是已经解析好的子树,这意味着三件事:其一,宏拿到的输入有结构保证——不会收到括号不配对、优先级歧义的"烂输入";其二,宏的改写是树的搬运与替换,不碰文本就不会制造文本事故;其三,宏的输出作为树重新进入编译流水线,类型检查、优化、代码生成一视同仁。Rust 过程宏的接口签名里,出入参都是语法树类型,Lisp 宏的出入参都是列表——同构性让"改写代码"与"写普通代码"使用同一套能力,这正是 1.1 同像性思想在工具层的兑现。
AST 的应用面远不止宏。代码格式化工具读树、按风格规则重新打印文本;静态检查器在树上匹配"危险模式"(比如把 == 用在浮点上);IDE 的重命名重构是"找到所有引用同一名字的树节点再统一替换";连 3.4 的动态生成,也越来越多地从"拼字符串"升级为"拼树"。可以说,谁会读树,谁就拿到了操纵代码的通行证。
各种树上作业千变万化,套路却是同一个三段式:
# 三段式:遍历 → 匹配 → 改写 class DeadCodeFinder(ast.NodeVisitor): def visit_If(self, node): # 匹配:if True 分支恒真,else 永不执行 if isinstance(node.test, ast.Constant) and node.test.value is True: print(f"恒真分支:第 {node.lineno} 行,else 体是死代码") self.generic_visit(node) # 继续深入子树 DeadCodeFinder().visit(ast.parse(source_code))
操作→结果:访问器按节点类型分发,匹配到"条件为常量真"的分支即报告死代码。解读:generic_visit 保证递归不漏子树——漏了它,嵌套结构里的同类问题就会逃过检查。变式:把"报告"换成"替换节点",同一个骨架就变成代码自动修复器;把匹配规则换成安全规则,就是一组静态安全检查。框架作者与 linter 作者写的其实是同一种程序。
问:不同语言的 AST 结构一样吗?学一门是否通用? 核心思想通用,节点语法各异。表达式是树、语句是序列、作用域是嵌套——这三层几乎放之四海皆准;差异在节点命名(有的叫 BinOp、有的叫 BinaryExpression)与语言特有结构(Python 的缩进语义、Lisp 的列表即代码)。掌握一门语言的正字法后,迁移成本主要在查文档,不在重学思想。
问:既然树丢了格式信息,格式化工具怎么还原代码? 靠"树加位置标注"。实用实现会在节点上附带原始行号列号,或使用保留全部细节的具象语法树(CST)。顺带澄清一对术语:AST 是抽象版(省略分号括号),CST 是具象版(一个标点不落)。做格式化与精确重构用 CST,做语义分析与代码生成用 AST——两者取用的正是同一个源文本的两面。
树是静态的表示,改写是静态的动作。下一节把生成能力整个搬进运行期:程序如何在现场制造出自己的新代码。