大家好,我是正在实战各种 AI 项目的程序员晚枫。

当你写下 print("hello"),解释器第一步做什么?把它切成一个个 Token。

词法分析是编译的第一步,理解 Tokenizer 的工作原理,能帮你理解 Python 的语法细节。

想象你在读一篇文章。首先,你要识别出每个词:名词、动词、形容词……词法分析器做的就是类似的工作。它把源代码这串字符,切成一个个有意义的"词",也就是 Token。


🔤 Token 是什么?

Token 是源代码的最小有意义单元。比如:

1
2
3
4
5
# 源代码
x = 1 + 2

# 对应的 Token 序列
NAME('x') EQ NUMBER(1) PLUS NUMBER(2) NEWLINE

每个 Token 包含两个信息:类型。比如 NAME('x') 表示这是一个标识符类型的 Token,值是'x'。

Token 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Include/token.h
typedef enum {
ENDMARKER, // 文件结束
NAME, // 标识符(变量名、函数名)
NUMBER, // 数字字面量
STRING, // 字符串字面量
NEWLINE, // 换行
INDENT, // 缩进增加(Python 特色)
DEDENT, // 缩进减少(Python 特色)
LPAR, RPAR, // ()
LSQB, RSQB, // []
LBRACE, RBRACE, // {}
COLON, // :
COMMA, // ,
SEMI, // ;
PLUS, MINUS, // + -
STAR, SLASH, // * /
VBAR, AMPER, // | &
LESS, GREATER, // < >
EQUAL, // ==
DOT, // .
PERCENT, // %
// ... 更多运算符和关键字
} token_type;

Token 分类

可以把 Token 分为几大类:

类别包含示例
标识符NAME变量名、函数名、类名
字面量NUMBER, STRING数字、字符串
运算符PLUS, MINUS, STAR+、-、* 等
分隔符LPAR, COMMA, COLON()、,、: 等
关键字IF, FOR, WHILEif、for、while 等
特殊INDENT, DEDENT, NEWLINEPython 特有

📝 Tokenizer 工作原理

核心流程

1
2
3
4
5
6
7
8
9
源代码字符流

[读取字符] → 判断字符类型

[识别 Token] → 根据规则生成 Token

[处理特殊逻辑] → 缩进、换行、续行

Token 序列

状态机设计

Tokenizer 本质上是一个状态机。它根据当前字符和状态,决定下一个状态和输出的 Token。

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
29
30
31
32
33
34
35
36
37
38
39
40
// Parser/tokenizer.c
struct tok_state {
char *buf; // 输入缓冲区
char *cur; // 当前位置
int lineno; // 当前行号
int indent; // 当前缩进级别
int *indent_stack; // 缩进栈
int atbol; // 在行首 (at beginning of line)
// ...
};

token_type PyTokenizer_Get(struct tok_state *tok, char **start, char **end)
{
for (;;) {
c = tok_nextc(tok); // 读取下一个字符

switch (c) {
case EOF:
return ENDMARKER;

case '\n':
// 处理换行和缩进
return handle_newline(tok);

case 'a'...'z', 'A'...'Z', '_':
// 标识符或关键字
return handle_identifier(tok, start, end);

case '0'...'9':
// 数字
return handle_number(tok, start, end);

case '"', "'":
// 字符串
return handle_string(tok, start, end);

// ... 更多 case
}
}
}

这个函数是 Tokenizer 的核心。它不断读取字符,根据字符类型调用不同的处理函数,最终返回 Token。


🎯 Python 特色的缩进处理

Python 用缩进表示代码块,这是 Tokenizer 最复杂的部分。

缩进栈机制

1
2
3
4
5
typedef struct {
int *stack; // 缩进级别栈
int depth; // 当前栈深度
int max_depth; // 栈最大容量
} IndentStack;

处理流程

让我们看一个具体的例子:

1
2
3
4
5
# 示例代码
def foo():
if True:
print(1)
print(2)

对应的 Token 序列:

1
2
3
4
5
6
7
8
NAME(def)   NAME(foo)   LPAR   RPAR   COLON   NEWLINE
INDENT(4) # 检测到缩进增加
NAME(if) NAME(True) COLON NEWLINE
INDENT(8) # 再次增加
NAME(print) LPAR NUMBER(1) RPAR NEWLINE
DEDENT(4) # 缩进减少
NAME(print) LPAR NUMBER(2) RPAR NEWLINE
DEDENT(0) # 回到无缩进

关键代码

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
29
static int
tok_get_indent(struct tok_state *tok, char **start, char **end)
{
int col = 0;

// 计算缩进列数
while (c == ' ') {
col++;
c = tok_nextc(tok);
}

// 与栈顶比较
int top = tok->indent_stack[tok->indent_level];

if (col > top) {
// 缩进增加,压栈
tok->indent_stack[++tok->indent_level] = col;
return INDENT;
} else if (col < top) {
// 缩进减少,出栈
while (col < tok->indent_stack[tok->indent_level]) {
tok->indent_level--;
// 需要返回多个 DEDENT
}
return DEDENT;
}

return -1; // 缩进不变
}

这段代码展示了缩进处理的核心逻辑:

  1. 计算当前行的缩进列数
  2. 与栈顶的缩进级别比较
  3. 根据比较结果返回 INDENT、DEDENT 或不变

💡 实战:使用 tokenize 模块

Python 标准库提供了 tokenize 模块,可以直接使用 Tokenizer:

查看代码的 Token

1
2
3
4
5
6
7
8
9
10
11
import tokenize
import io

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

# 将代码转为 Token
for tok in tokenize.generate_tokens(io.StringIO(code).readline):
print(f"{tokenize.tok_name[tok.type]:15} {tok.string!r:15} {tok.start}")

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ENCODING       'utf-8'         (0, 0)
NL '\n' (1, 0)
NAME 'def' (2, 0)
NAME 'hello' (2, 4)
OP '(' (2, 9)
OP ')' (2, 10)
OP ':' (2, 11)
NEWLINE '\n' (2, 12)
INDENT ' ' (3, 0)
NAME 'print' (3, 4)
OP '(' (3, 9)
STRING '"world"' (3, 10)
OP ')' (3, 17)
NEWLINE '\n' (3, 18)
DEDENT '' (4, 0)
ENDMARKER '' (4, 0)

Token 信息

每个 Token 包含以下信息:

属性含义
typeToken 类型(数字编码)
stringToken 的实际字符串
start起始位置(行号,列号)
end结束位置(行号,列号)
line原始代码行

⚠️ 常见问题与技巧

1. 关键字 vs 标识符

1
2
3
4
5
6
# 'if' 是关键字
if True:
pass

# 'iffy' 是标识符
iffy = True

Tokenizer 如何区分?它会先识别为 NAME,然后查关键字表。如果在关键字表中,就返回关键字 Token。

2. 多行字符串

1
2
3
4
5
# 三引号字符串可以跨多行
text = """
第一行
第二行
"""

Tokenizer 需要特殊处理三引号,跟踪是否遇到结束标记。

3. 续行符

1
2
3
4
5
6
7
# 反斜杠续行
total = a + b + \
c + d

# 括号内自动续行
total = (a + b +
c + d)

反斜杠续行需要特殊处理,而括号内的换行不需要。


🎯 本讲总结

通过本讲,我们深入理解了:

Token 的概念:源代码的最小有意义单元,包含类型和值。

Tokenizer 工作流程:字符流→Token 序列的状态机转换。

Python 缩进处理:INDENT/DEDENT 的生成机制,缩进栈的使用。

标准库 tokenize 模块:如何使用 Tokenizer 分析代码。

常见问题:关键字识别、多行字符串、续行处理等。

这些知识是理解后续语法分析的基础。


📚 推荐教材

《Python 编程从入门到实践(第 3 版)》 - Eric Matthes 著

Python 零基础入门首选。本书分为基础语法和项目实战两部分,适合完全没有编程经验的读者。

《流畅的 Python(第 2 版)》 - Luciano Ramalho 著

Python 进阶经典之作。深入讲解 Python 的高级特性,包括数据模型、函数式编程、面向对象、元编程等。

《CPython 设计与实现》 - Anthony Shaw 著

本书深入讲解 CPython 内部机制,从内存管理到字节码执行,从对象模型到并发编程。配合本课程学习,效果更佳。

学习路线建议:

1
零基础 → 《从入门到实践》 → 《流畅的 Python》 → 本门课程 → 《CPython 设计与实现》

🔗 课程导航

上一讲:垃圾回收机制详解 | 下一讲:语法分析与 AST 生成


💬 联系我

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

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

🎓 AI 编程实战课程

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