手写一个迷你 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 有一个真实的编译器和虚拟机。


思考 && 总结

迷你解释器告诉我们三件事:

  1. 解释器没有魔法——它只是三层翻译器:字符 → Token → AST → 结果。 每一层各自独立,各自做一次翻译。
  2. 递归下降解析器非常适合入门——每个语法规则是一个函数,直接映射到数学定义的语法公式。
  3. 500 行 Python 足够实现一个变量的计算器。 扩展它支持 if/while/函数定义/函数调用,就是逐步走向"真正的"解释器。

这个迷你解释器的代码体现了基础但正确的解释器架构——正是 CPython 架构的微缩版。


结尾

第二板块"源码拆解"十篇全部完结。感谢各位一路读来!

源码骑士 — 源码级拆解,从底层看透技术

👀 关注:跟博主一起从源码视角深耕底层原理

❤️ 点赞:让优质内容被更多人看见

收藏:核心知识点存好,随用随查

💬 评论:分享你的经验或疑问,一起交流

🔄 一键四连:别忘了给博主一键四连!

🗡️ 寄语:写一个解释器,是理解一个语言的最快方法。

结语:从 GIL 到列表扩容公式、从 copy 模块源码到 __slots__ 描述符、再到生成器的帧暂停、最后手写解释器——第二板块的目标是"让你看着 Python 代码时,脑子里有一张 C 层的内存图"。第三板块进入工程落地——Django、Docker部署、性能优化——不见不散!一键四连!

更多推荐