大家好,我是正在实战各种 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

# Token 序列
NAME(x) EQ NUMBER(1) PLUS NUMBER(2) STAR NUMBER(3)

# AST(简化表示)
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=[]
)

# If 语句
ast.If(
test=ast.Constant(True),
body=[...],
orelse=[...]
)

# For 循环
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
// Parser/parser.c
mod_ty PyParser_ASTFromFileObject(...) {
// 1. 创建 Parser 状态
Parser *p = _PyPegen_Parser_New(...);

// 2. 运行生成的解析器
mod_ty result = run_parser(p);

// 3. 返回 AST 模块节点
return result;
}

错误处理

语法分析器不仅要生成 AST,还要报告语法错误:

1
2
3
4
5
# 语法错误示例
x = 1 + # 缺少操作数

# 错误信息
# SyntaxError: invalid syntax

解析器会指出错误的位置和类型,帮助开发者定位问题。


💡 实战:使用 ast 模块

查看代码的 AST

1
2
3
4
5
6
7
8
9
10
11
12
import ast

code = '''
def hello():
print("world")
'''

# 解析为 AST
tree = ast.parse(code)

# 打印 AST 结构
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 语句
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 # pip install 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 属性,表示是读取还是写入:

1
2
3
4
5
# 读取
x = 1 # x 的 ctx 是 Store()

# 写入
print(x) # x 的 ctx 是 Load()

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 | 下一讲:字节码编译器


💬 联系我

平台账号/链接
微信扫码加好友
微博@程序员晚枫
知乎@程序员晚枫
抖音@程序员晚枫
小红书@程序员晚枫
B 站Python 自动化办公社区

主营业务:AI 编程培训、企业内训、技术咨询

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!