大家好,我是正在实战各种 AI 项目的程序员晚枫。
Tokenizer 把代码切成 Token 后,如何理解这些 Token 的语法结构?这就是语法分析器的工作。
理解 AST(抽象语法树),是掌握 Python 代码分析和转换的基础。
想象你拿到一堆乐高积木(Token)。语法分析器的任务就是按照说明书(语法规则),把这些积木组装成一个完整的模型(AST)。这个模型反映了代码的结构,便于后续的编译和执行。
🌳 什么是 AST?
AST 是源代码的树形表示,去除了具体的语法细节(如括号、逗号),保留了语义结构。
示例对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| x = 1 + 2 * 3
NAME(x) EQ NUMBER(1) PLUS NUMBER(2) STAR NUMBER(3)
Assign( targets=[Name(id='x', ctx=Store())], value=BinOp( left=Constant(value=1), op=Add(), right=BinOp( left=Constant(value=2), op=Mult(), right=Constant(value=3) ) ) )
|
AST 的可视化
1 2 3 4 5 6 7 8 9
| Assign / \ Name BinOp | / \ 'x' Constant BinOp | / \ 1 Constant Constant | | 2 3
|
这个树形结构清晰地展示了代码的层次关系:
- 根节点是赋值操作(Assign)
- 左边是变量名 x
- 右边是加法操作(1 + 2*3)
- 加法右边又是一个乘法操作(2*3)
为什么需要 AST?
AST 相比源代码有几个优势:
| 优势 | 说明 |
|---|
| 结构清晰 | 树形结构直观反映代码层次 |
| 易于分析 | 可以遍历 AST 分析代码特征 |
| 易于转换 | 可以修改 AST 生成新代码 |
| 去除冗余 | 去掉括号、逗号等语法细节 |
🔍 Python 的 AST 节点类型
Python 的 AST 节点分为几大类:
表达式节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import ast
ast.Constant(value=42)
ast.Name(id='x', ctx=ast.Load()) ast.Name(id='x', ctx=ast.Store())
ast.BinOp( left=ast.Constant(1), op=ast.Add(), right=ast.Constant(2) )
ast.Call( func=ast.Name('print'), args=[ast.Constant('hello')], keywords=[] )
ast.List( elts=[ast.Constant(1), ast.Constant(2)], ctx=ast.Load() )
|
语句节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| ast.Assign( targets=[ast.Name('x')], value=ast.Constant(1) )
ast.FunctionDef( name='foo', args=ast.arguments(...), body=[...], decorator_list=[] )
ast.If( test=ast.Constant(True), body=[...], orelse=[...] )
ast.For( target=ast.Name('i'), iter=ast.Call(...), body=[...] )
|
完整的 AST 示例
1 2 3 4 5
| def factorial(n): if n <= 1: return 1 return n * factorial(n - 1)
|
对应的 AST(简化):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Module └── FunctionDef(name='factorial') ├── args=[arg(arg='n')] └── body=[ If( test=Compare(...), body=[Return(value=Constant(1))], orelse=[] ), Return( value=BinOp( left=Name('n'), op=Mult(), right=Call(func=Name('factorial'), ...) ) ) ]
|
⚙️ Parser 工作原理
PEG 语法
Python 3.9+ 使用 PEG(Parsing Expression Grammar)解析器,替代了之前的 LL(1) 解析器。
PEG 语法更强大,可以表达更复杂的语法规则。以下是简化示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| # Grammar/python.gram(简化)
file[mod_ty]: a=[statements] ENDMARKER { _PyPegen_make_module(p, a) }
statements[asdl_seq*]: a=statement+ { a }
statement[stmt_ty]: | compound_stmt | simple_stmt
simple_stmt[stmt_ty]: | assignment | expressions | return_stmt | import_stmt | ...
assignment[stmt_ty]: | a=NAME ':' b=expression c=['=' d=expression { d }] { _Py_AnnAssign(...) } | a=(z=star_targets '=' { z })+ b=(yield_expr | star_expressions) { _Py_Assign(a, b, NULL, EXTRA) }
|
解析过程
1 2 3 4 5 6 7 8 9 10 11
| mod_ty PyParser_ASTFromFileObject(...) { Parser *p = _PyPegen_Parser_New(...); mod_ty result = run_parser(p); return result; }
|
错误处理
语法分析器不仅要生成 AST,还要报告语法错误:
解析器会指出错误的位置和类型,帮助开发者定位问题。
💡 实战:使用 ast 模块
查看代码的 AST
1 2 3 4 5 6 7 8 9 10 11 12
| import ast
code = ''' def hello(): print("world") '''
tree = ast.parse(code)
print(ast.dump(tree, indent=2))
|
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Module( body=[ FunctionDef( name='hello', args=arguments(...), body=[ Expr( value=Call( func=Name(id='print'), args=[Constant(value='world')], keywords=[] ) ) ], decorator_list=[] ) ], type_ignores=[] )
|
AST 遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class FunctionCounter(ast.NodeVisitor): """统计函数定义数量""" def __init__(self): self.count = 0 def visit_FunctionDef(self, node): self.count += 1 print(f"发现函数:{node.name}") self.generic_visit(node)
counter = FunctionCounter() counter.visit(tree) print(f"总共 {counter.count} 个函数")
|
代码转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class AddPrintTransformer(ast.NodeTransformer): """在每个函数开头添加 print 语句""" def visit_FunctionDef(self, node): print_stmt = ast.Expr( value=ast.Call( func=ast.Name(id='print'), args=[ast.Constant(value=f"进入函数:{node.name}")], keywords=[] ) ) node.body.insert(0, print_stmt) ast.fix_missing_locations(node) return node
transformer = AddPrintTransformer() new_tree = transformer.visit(tree)
import astor new_code = astor.to_source(new_tree) print(new_code)
|
输出:
1 2 3
| def hello(): print('进入函数:hello') print('world')
|
⚠️ 常见问题与技巧
1. 上下文(ctx)
AST 中的 Name 节点有 ctx 属性,表示是读取还是写入:
2. 行号和列号
AST 节点包含位置信息,用于错误报告:
1 2 3
| for node in ast.walk(tree): if hasattr(node, 'lineno'): print(f"{node.__class__.__name__} at line {node.lineno}")
|
3. 修复位置信息
修改 AST 后,需要修复位置信息:
1
| ast.fix_missing_locations(new_tree)
|
🎯 本讲总结
通过本讲,我们深入理解了:
AST 的概念:源代码的树形语义表示,去除语法细节,保留语义结构。
AST 节点类型:表达式节点、语句节点等各类节点的结构和用途。
Parser 工作原理:PEG 语法解析流程,从 Token 到 AST 的转换。
ast 模块实战:解析、遍历、修改 AST 的方法和技巧。
常见问题:上下文、位置信息、修复等。
这些知识是理解后续字节码编译的基础。
📚 推荐教材
《Python 编程从入门到实践(第 3 版)》 - Eric Matthes 著
Python 零基础入门首选。本书分为基础语法和项目实战两部分,适合完全没有编程经验的读者。
《流畅的 Python(第 2 版)》 - Luciano Ramalho 著
Python 进阶经典之作。深入讲解 Python 的高级特性,包括数据模型、函数式编程、面向对象、元编程等。
《CPython 设计与实现》 - Anthony Shaw 著
本书深入讲解 CPython 内部机制,从内存管理到字节码执行,从对象模型到并发编程。配合本课程学习,效果更佳。
学习路线建议:
1
| 零基础 → 《从入门到实践》 → 《流畅的 Python》 → 本门课程 → 《CPython 设计与实现》
|
🔗 课程导航
← 上一讲:词法分析器 Tokenizer | 下一讲:字节码编译器 →
💬 联系我
主营业务:AI 编程培训、企业内训、技术咨询
🎓 AI 编程实战课程
想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!