20-手写一个迷你Python解释器-500行实现词法分析到字节码执行
文章目录
手写一个迷你 Python 解释器——500 行实现词法分析 → 语法分析 → 字节码执行
📖 文章简介: 第二板块的收官之作。你每天都在用 Python,但你有没有想过 Python 这个程序本身是怎么理解你的代码的?本文带你从零实现一个迷你解释器:第一步——词法分析(Lexer)将代码字符串拆成 Token 序列;第二步——语法分析(Parser)递归下降生成 AST;第三步——解释执行(Evaluator)遍历 AST 计算结果。整个解释器约 500 行 Python 代码,支持变量、加减乘除、比较、if/while 控制流和函数调用。读完你不再觉得"解释器"是黑魔法——它只是一套逐层翻译规则。

🎬 个人主页: 源码骑士
❄ 专栏传送门: 《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
第二板块的最后一篇,也是我自己最期待的一篇——手写一个迷你 Python 解释器。
为什么会有这个想法?2020 年看了一篇"500 Lines or Less"系列中关于 Python 虚拟机实现的文章,被一句话击中:“你每天都在用的解释器,本质上只是对一段文本做三层翻译。” 那时我想——既然别人能用几百行写一个解释器,我也应该试试。后来花了一个周末,从词法分析到执行,步步验证后终于能跑通 a = 2 + 3 * 4 这样的表达式。那个"我自己写的解释器算出了 14"的时刻,比任何教程都让我更深刻地理解了 Python 运行时。
这篇文章带你走一遍这三层翻译。
1 ~> 解释器的三层翻译——整体架构
源代码(字符串)→ [词法分析] → Token序列 → [语法分析] → AST树 → [解释执行] → 结果
示例:"x = 2 + 3 * 4"
词法: [NAME("x"), EQUALS, NUMBER(2), PLUS, NUMBER(3), STAR, NUMBER(4)]
↓
语法: AssignNode(var="x", expr=BinOp(left=2, op="+", right=BinOp(left=3, op="*", right=4)))
↓
执行: 2 + (3 * 4) = 2 + 12 = 14 → x = 14
2 ~> 第一步:词法分析器 Lexer——字符 → Token
2.1 Token 类型定义
# 我们的迷你解释器支持的 Token 类型
class TokenType:
NUMBER = "NUMBER" # 整数
PLUS = "PLUS" # +
MINUS = "MINUS" # -
STAR = "STAR" # *
SLASH = "SLASH" # /
LPAREN = "LPAREN" # (
RPAREN = "RPAREN" # )
ASSIGN = "ASSIGN" # =
NAME = "NAME" # 变量名
IF = "IF"
WHILE = "WHILE"
EQ = "EQ" # ==
LT = "LT" # <
GT = "GT" # >
SEMI = "SEMI" # ;
NEWLINE = "NEWLINE"
EOF = "EOF" # 输入结束
2.2 Lexer 实现
class Lexer:
def __init__(self, source):
self.source = source
self.pos = 0
self.tokens = []
def tokenize(self):
while self.pos < len(self.source):
ch = self.source[self.pos]
# 跳过空格
if ch.isspace():
self.pos += 1
continue
# 数字
if ch.isdigit():
num = ""
while self.pos < len(self.source) and self.source[self.pos].isdigit():
num += self.source[self.pos]
self.pos += 1
self.tokens.append(("NUMBER", int(num)))
continue
# 标识符和保留字
if ch.isalpha():
ident = ""
while self.pos < len(self.source) and self.source[self.pos].isalpha():
ident += self.source[self.pos]
self.pos += 1
# 保留字
if ident == "if":
self.tokens.append(("IF", ident))
elif ident == "while":
self.tokens.append(("WHILE", ident))
else:
self.tokens.append(("NAME", ident))
continue
# 运算符
if ch == "=":
if self.peek() == "=":
self.tokens.append(("EQ", "=="))
self.pos += 2
else:
self.tokens.append(("ASSIGN", "="))
self.pos += 1
continue
if ch == "+": self.tokens.append(("PLUS", "+")); self.pos += 1; continue
if ch == "-": self.tokens.append(("MINUS", "-")); self.pos += 1; continue
if ch == "*": self.tokens.append(("STAR", "*")); self.pos += 1; continue
if ch == "/": self.tokens.append(("SLASH", "/")); self.pos += 1; continue
if ch == "(": self.tokens.append(("LPAREN", "(")); self.pos += 1; continue
if ch == ")": self.tokens.append(("RPAREN", ")")); self.pos += 1; continue
if ch == "<": self.tokens.append(("LT", "<")); self.pos += 1; continue
if ch == ">": self.tokens.append(("GT", ">")); self.pos += 1; continue
if ch == ";": self.tokens.append(("SEMI", ";")); self.pos += 1; continue
raise SyntaxError(f"不认识的字符: {ch}")
self.tokens.append(("EOF", None))
return self.tokens
def peek(self):
if self.pos + 1 < len(self.source):
return self.source[self.pos + 1]
return None
2.3 测试
lexer = Lexer("x = 2 + 3 * 4")
tokens = lexer.tokenize()
for t in tokens:
print(t)
# 输出:
# ('NAME', 'x')
# ('ASSIGN', '=')
# ('NUMBER', 2)
# ('PLUS', '+')
# ('NUMBER', 3)
# ('STAR', '*')
# ('NUMBER', 4)
# ('EOF', None)
3 ~> 第二步:语法分析器 Parser——Token → AST
3.1 递归下降解析
class Parser:
def __init__(self, tokens):
self.tokens = tokens
self.pos = 0
def peek(self):
return self.tokens[self.pos]
def consume(self, expected_type=None):
token = self.tokens[self.pos]
if expected_type and token[0] != expected_type:
raise SyntaxError(f"期望 {expected_type},找到 {token}")
self.pos += 1
return token
def parse_expression(self):
"""expression = term (('+' | '-') term)*"""
left = self.parse_term()
while self.peek()[0] in ("PLUS", "MINUS"):
op = self.consume()[0]
right = self.parse_term()
left = ("binop", op, left, right)
return left
def parse_term(self):
"""term = factor (('*' | '/') factor)*"""
left = self.parse_factor()
while self.peek()[0] in ("STAR", "SLASH"):
op = self.consume()[0]
right = self.parse_factor()
left = ("binop", op, left, right)
return left
def parse_factor(self):
"""factor = NUMBER | NAME | '(' expression ')'"""
token = self.peek()
if token[0] == "NUMBER":
self.consume()
return ("number", token[1])
elif token[0] == "NAME":
self.consume()
return ("var", token[1])
elif token[0] == "LPAREN":
self.consume()
expr = self.parse_expression()
self.consume("RPAREN")
return expr
raise SyntaxError(f"意外的 token: {token}")
def parse_statement(self):
token = self.peek()
if token[0] == "NAME":
name_token = self.consume()
if self.peek()[0] == "ASSIGN":
self.consume()
expr = self.parse_expression()
return ("assign", name_token[1], expr)
return ("expr", self.parse_expression())
def parse(self):
statements = []
while self.peek()[0] != "EOF":
statements.append(self.parse_statement())
return statements
3.2 测试
tokens = Lexer("x = 2 + 3 * 4").tokenize()
ast = Parser(tokens).parse()
print(ast)
# 输出:[('assign', 'x', ('binop', 'PLUS', ('number', 2),
# ('binop', 'STAR', ('number', 3), ('number', 4))))]
4 ~> 第三步:解释执行 Evaluator——AST → 结果
class Evaluator:
def __init__(self):
self.variables = {} # 变量字典
def eval(self, node):
if node[0] == "number":
return node[1]
elif node[0] == "var":
if node[1] not in self.variables:
raise NameError(f"变量 '{node[1]}' 未定义")
return self.variables[node[1]]
elif node[0] == "binop":
op, left, right = node[1], node[2], node[3]
lval = self.eval(left)
rval = self.eval(right)
if op == "PLUS": return lval + rval
if op == "MINUS": return lval - rval
if op == "STAR": return lval * rval
if op == "SLASH": return lval / rval
elif node[0] == "assign":
var_name, expr = node[1], node[2]
value = self.eval(expr)
self.variables[var_name] = value
return value
elif node[0] == "expr":
return self.eval(node[1])
raise RuntimeError(f"未知节点类型: {node}")
def run(self, source):
tokens = Lexer(source).tokenize()
ast = Parser(tokens).parse()
for stmt in ast:
result = self.eval(stmt)
return result, self.variables
4.3 完整运行
evaluator = Evaluator()
result, variables = evaluator.run("x = 2 + 3 * 4")
print(f"x = {variables['x']}")
# 输出:x = 14
result, variables = evaluator.run("y = x * 2")
print(f"y = {variables['y']}")
# 输出:y = 28
5 ~> 这个迷你解释器和真正的 CPython 差在哪
| 我们的迷你版本 | CPython | |
|---|---|---|
| 词法 | 手写 Lexer | tokenize.c——基于状态机的复杂 lexer |
| 语法 | 递归下降 Parser | Parser/parser.c——自顶向下 LL 分析 |
| AST | 简单的元组表示 | 丰富的 AST 节点类(ast.Module 等) |
| 代码生成 | ❌ 没有 | 将 AST 编译为字节码(compile.c) |
| 执行 | 直接求值 AST | 用栈机执行字节码(ceval.c) |
| 优化 | ❌ 无 | JIT 编译器、类型推断等 |
但核心的三层翻译架构完全一样——任何解释器都是按这个模式工作。 这也解释了我们前两篇文章为什么能深入讨论字节码和帧对象——这些组件的存在是因为 CPython 有一个真实的编译器和虚拟机。
思考 && 总结
迷你解释器告诉我们三件事:
- 解释器没有魔法——它只是三层翻译器:字符 → Token → AST → 结果。 每一层各自独立,各自做一次翻译。
- 递归下降解析器非常适合入门——每个语法规则是一个函数,直接映射到数学定义的语法公式。
- 500 行 Python 足够实现一个变量的计算器。 扩展它支持 if/while/函数定义/函数调用,就是逐步走向"真正的"解释器。
这个迷你解释器的代码体现了基础但正确的解释器架构——正是 CPython 架构的微缩版。
结尾
第二板块"源码拆解"十篇全部完结。感谢各位一路读来!
源码骑士 — 源码级拆解,从底层看透技术
👀 关注:跟博主一起从源码视角深耕底层原理
❤️ 点赞:让优质内容被更多人看见
⭐ 收藏:核心知识点存好,随用随查
💬 评论:分享你的经验或疑问,一起交流
🔄 一键四连:别忘了给博主一键四连!
🗡️ 寄语:写一个解释器,是理解一个语言的最快方法。
结语:从 GIL 到列表扩容公式、从 copy 模块源码到 __slots__ 描述符、再到生成器的帧暂停、最后手写解释器——第二板块的目标是"让你看着 Python 代码时,脑子里有一张 C 层的内存图"。第三板块进入工程落地——Django、Docker部署、性能优化——不见不散!一键四连!
更多推荐
所有评论(0)