这是一个非常棒的问题,也是理解编程语言本质的绝佳方式!用Python写一个Python编译器,听起来像是一个“俄罗斯套娃”,但实际上是完全可行的,并且是学习编译器原理的经典实践。

我们需要明确一个关键概念:我们通常不会从头开始写一个编译器,编译器是一个非常复杂的系统,包含词法分析、语法分析、语义分析、中间代码生成、优化和目标代码生成等多个阶段,即使是Python这样的高级语言,其官方CPython编译器也是一个极其庞大的项目(用C语言编写)。
当说“用Python写一个Python编译器”时,我们通常指的是以下几种情况,我会从简单到复杂为你介绍:
- 最简单的解释器:只支持极少数Python语法,用于教学目的。
- 使用解析器生成工具:这是最实用、最接近“真·编译器”的入门方法。
- 最真实的挑战:实现一个功能完整的、能将Python编译成机器码的编译器(这是一个博士级别的项目)。
从零开始写一个微型Python解释器
这种方法不依赖任何外部库,能让你深刻理解解释器是如何工作的,我们将实现一个非常简化的版本,只支持整数加减乘除和变量赋值。
核心思想
我们将代码读入一个字符串,然后手动实现一个递归下降解析器 来解析它,最后直接执行解析后的结构。

步骤 1: 词法分析
将代码字符串分解成一个个有意义的“词”,称为 Token。"10 + 20" 会被分解成 NUMBER(10), PLUS, NUMBER(20)。
import re
from enum import Enum, auto
class TokenType(Enum):
NUMBER = auto()
PLUS = auto()
MINUS = auto()
MULTIPLY = auto()
DIVIDE = auto()
LPAREN = auto() # (
RPAREN = auto() # )
ASSIGN = auto() # =
ID = auto() # 变量名 (e.g., x, my_var)
EOF = auto() # 文件结束符
class Token:
def __init__(self, type, value):
self.type = type
self.value = value
def __repr__(self):
return f"Token({self.type}, {repr(self.value)})"
def tokenize(code):
token_specification = [
('NUMBER', r'\d+'), # Integer
('ASSIGN', r'='), # Assignment operator
('PLUS', r'\+'), # Addition operator
('MINUS', r'-'), # Subtraction operator
('MULTIPLY', r'\*'), # Multiplication operator
('DIVIDE', r'/'), # Division operator
('LPAREN', r'\('), # Left parenthesis
('RPAREN', r'\)'), # Right parenthesis
('ID', r'[a-zA-Z_][a-zA-Z0-9_]*'), # Identifiers
('SKIP', r'[ \t]'), # Skip over whitespace
('MISMATCH', r'.'), # Any other character
]
tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification)
for mo in re.finditer(tok_regex, code):
kind = mo.lastgroup
value = mo.group()
if kind == 'NUMBER':
value = int(value)
elif kind == 'SKIP':
continue
elif kind == 'MISMATCH':
raise RuntimeError(f'Unexpected character: {value}')
yield Token(TokenType[kind], value)
步骤 2: 语法分析与解释
现在我们有了Token流,需要解析它们并计算结果,我们将使用递归下降的方法,为不同的语法结构(比如表达式、语句)编写对应的解析函数。
class Interpreter:
def __init__(self):
self._current_token = None
self._tokens = None
self._env = {} # 环境变量,用于存储变量
def error(self):
raise Exception('Error parsing input')
def eat(self, token_type):
# 检查当前token类型是否匹配期望的类型
if self._current_token.type == token_type:
try:
self._current_token = next(self._tokens)
except StopIteration:
self._current_token = None
else:
self.error()
def factor(self):
""" factor : INTEGER | LPAREN expr RPAREN | ID """
token = self._current_token
if token.type == TokenType.NUMBER:
self.eat(TokenType.NUMBER)
return token.value
elif token.type == TokenType.LPAREN:
self.eat(TokenType.LPAREN)
result = self.expr()
self.eat(TokenType.RPAREN)
return result
elif token.type == TokenType.ID:
var_name = token.value
self.eat(TokenType.ID)
if var_name not in self._env:
raise NameError(f"Name '{var_name}' is not defined")
return self._env[var_name]
def term(self):
""" term : factor ((MUL | DIV) factor)* """
result = self.factor()
while self._current_token and self._current_token.type in (TokenType.MULTIPLY, TokenType.DIVIDE):
token = self._current_token
if token.type == TokenType.MULTIPLY:
self.eat(TokenType.MULTIPLY)
result *= self.factor()
elif token.type == TokenType.DIVIDE:
self.eat(TokenType.DIVIDE)
result //= self.factor() # 使用整数除法
return result
def expr(self):
"""
expr : term ((PLUS | MINUS) term)*
term : factor ((MUL | DIV) factor)*
factor : INTEGER | LPAREN expr RPAREN
"""
# 语法分析 + 解释执行
result = self.term()
while self._current_token and self._current_token.type in (TokenType.PLUS, TokenType.MINUS):
token = self._current_token
if token.type == TokenType.PLUS:
self.eat(TokenType.PLUS)
result += self.term()
elif token.type == TokenType.MINUS:
self.eat(TokenType.MINUS)
result -= self.term()
return result
def parse_assignment(self):
""" assignment : ID ASSIGN expr """
var_name = self._current_token.value
self.eat(TokenType.ID)
self.eat(TokenType.ASSIGN)
value = self.expr()
self._env[var_name] = value
return value
def run(self, code):
self._tokens = tokenize(code)
self._current_token = next(self._tokens)
# 简单起见,我们只处理赋值语句或表达式
if self._current_token.type == TokenType.ID and self._tokens.peek().type == TokenType.ASSIGN:
result = self.parse_assignment()
else:
result = self.expr()
if self._current_token is not None:
self.error()
return result
步骤 3: 运行你的微型解释器
# 主程序
if __name__ == '__main__':
interpreter = Interpreter()
while True:
try:
code = input('calc> ')
except EOFError:
break
if not code:
continue
try:
result = interpreter.run(code)
print(result)
except Exception as e:
print(e)
运行示例:
calc> 10 + 20 * 2
50
calc> (10 + 20) * 2
60
calc> x = 100
100
calc> x + 5
105
calc> y = x * 2
200
calc> y / 10
20
优点:

- 完全不依赖外部库,能让你从底层理解解析和执行的原理。
- 代码量相对较少,易于学习和修改。
缺点:
- 实现复杂的语法(如
if/else,for循环)会变得异常困难。 - 错误处理能力很弱。
- 性能低下,因为边解析边执行。
使用解析器生成工具(推荐)
这是专业和半专业项目中更常用的方法,我们使用一个工具(如 sly 或 ply)来帮我们完成最复杂的部分——语法分析,我们只需要定义好语法规则,工具就能自动生成一个解析器。
我们将使用 sly (Sly Lex-Yacc),一个现代、易用的Python版Lex/Yacc。
步骤 1: 安装 sly
pip install sly
步骤 2: 编写词法分析器和语法分析器
我们将把词法分析和语法分析写在一个类里,并实现一个简单的求值器。
from sly import Lexer, Parser
class MiniPythonLexer(Lexer):
# 定义Token列表(按优先级排序)
tokens = { NAME, NUMBER, PLUS, MINUS, TIMES, DIVIDE, EQ }
# 忽略的字符
ignore = ' \t'
# 定义Token的正则表达式
NAME = r'[a-zA-Z_][a-zA-Z0-9_]*'
NUMBER = r'\d+'
# 定义简单Token
PLUS = r'\+'
MINUS = r'-'
TIMES = r'\*'
DIVIDE = r'/'
EQ = r'='
# 在遇到换行符时更新行号
@_(r'\n+')
def ignore_newline(self, t):
self.lineno += t.value.count('\n')
# 错误处理
def error(self, t):
print(f"Illegal character '{t.value[0]}' at line {self.lineno}")
self.index += 1
class MiniPythonParser(Parser):
# 获取Token列表
tokens = MiniPythonLexer.tokens
# 语法规则和对应的操作
# 优先级(从低到高)
precedence = (
('left', PLUS, MINUS),
('left', TIMES, DIVIDE),
)
@_('expr')
def statement(self, p):
return p.expr
@_('expr PLUS expr')
def expr(self, p):
return p.expr0 + p.expr1
@_('expr MINUS expr')
def expr(self, p):
return p.expr0 - p.expr1
@_('expr TIMES expr')
def expr(self, p):
return p.expr0 * p.expr1
@_('expr DIVIDE expr')
def expr(self, p):
return p.expr0 / p.expr1
@_('NUMBER')
def expr(self, p):
return int(p.NUMBER)
@_('NAME')
def expr(self, p):
# 这里我们用一个简单的字典作为环境
if p.NAME not in self.env:
raise NameError(f"Undefined name '{p.NAME}'")
return self.env[p.NAME]
@_('NAME EQ expr')
def statement(self, p):
self.env[p.NAME] = p.expr
return p.expr
# 构造函数,用于传递环境变量
def __init__(self):
self.env = {}
if __name__ == '__main__':
lexer = MiniPythonLexer()
parser = MiniPythonParser()
while True:
try:
text = input('minipy> ')
except EOFError:
break
if text:
try:
result = parser.parse(lexer.tokenize(text))
if result is not None:
print(result)
except Exception as e:
print(e)
运行示例:
minipy> 10 + 20 * 2
50
minipy> x = 100
100
minipy> y = x * 2
200
minipy> x + y
300
优点:
- 语法简洁:语法规则 (
_装饰的方法) 清晰地反映了语言的语法结构。 - 可扩展性强:添加新的语法规则(如
if,for,函数)相对容易。 - 专业标准:这是构建编译器/解释器的标准方法之一。
缺点:
- 仍然需要手动实现语义分析和执行部分。
- 错误报告可能不如大型框架(如ANTLR)详细。
实现一个真正的Python编译器
这是终极目标,也是难度最高的,一个完整的编译器通常包含以下阶段:
-
前端:
- 词法分析: 将源代码转换为Token流。(可以用
sly完成) - 语法分析: 将Token流转换成抽象语法树,AST是源代码的树状结构表示。(可以用
sly或ast模块) - 语义分析: 检查AST的语义是否正确,比如变量是否已声明、类型是否匹配等。
- 词法分析: 将源代码转换为Token流。(可以用
-
中端:
- 中间代码生成: 将AST转换成一种与机器无关的、更简单的中间表示,三地址码。
-
后端:
- 优化: 对中间代码进行优化,使其更高效。
- 目标代码生成: 将优化后的中间代码转换成特定机器的汇编语言或机器码。
如何实现?
- 目标: 编译成C代码或汇编代码,然后调用GCC/Clang等外部工具链来生成可执行文件,这是一个非常现实的目标。
- 挑战:
- Python的动态性: Python的类型是动态的,这使得编译时优化变得非常困难,你需要实现一个运行时类型系统来支持
isinstance,getattr等操作。 - 巨大的标准库: 你的编译器需要知道如何处理Python内置函数和类型。
- 字节码 vs 机器码: CPython解释器实际上是将Python源码编译成字节码,然后在虚拟机上执行,一个更“简单”的Python编译器可以模仿这个过程,生成自己的字节码和虚拟机。
- Python的动态性: Python的类型是动态的,这使得编译时优化变得非常困难,你需要实现一个运行时类型系统来支持
一个简化版的思路(生成字节码)
你可以模仿CPython,实现一个能生成.pyc字节码文件的编译器,这需要深入理解 Python字节码 的规范。
- 使用
sly解析Python源码,生成AST。 - 遍历AST,将其转换为Python字节码指令列表。
- 将字节码指令序列化成
.pyc文件格式。 - (可选)实现一个简单的虚拟机来执行你生成的字节码。
这个项目非常庞大,但也是理解Python内部工作原理的最佳途径。
总结与建议
| 方案 | 难度 | 核心工具 | 适用场景 | 学习价值 |
|---|---|---|---|---|
| 微型解释器 | ★☆☆☆☆ | 无 | 教学,快速原型 | ★★★★★ (理解底层原理) |
| 解析器生成工具 | ★★★☆☆ | sly, ply |
构建DSL,小型语言 | ★★★★☆ (行业标准实践) |
| 真实编译器 | ★★★★★ | 自定义 + 外部工具链 | 学术研究,大型项目 | ★★★★★ (终极挑战) |
给你的建议:
- 从方案一(微型解释器)开始:亲手实现一遍,你会对“解析”和“执行”有最直观的感受。
- 然后尝试方案二(
sly):对比一下你会发现,sly让代码结构清晰了无数倍,并且更容易扩展,这是构建任何语言工具的必经之路。 - 最后挑战方案三:当你对前两个方案非常熟悉后,可以尝试去阅读CPython的源码,或者研究一下像 mypyc (CPython的AOT编译器) 这样的项目,它们是实现“真·Python编译器”的绝佳参考。
祝你编译器开发之旅愉快!这是一个充满挑战和乐趣的过程。
