杰瑞科技汇

Python写Python编译器?性能如何实现?

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

Python写Python编译器?性能如何实现?-图1
(图片来源网络,侵删)

我们需要明确一个关键概念:我们通常不会从头开始写一个编译器,编译器是一个非常复杂的系统,包含词法分析、语法分析、语义分析、中间代码生成、优化和目标代码生成等多个阶段,即使是Python这样的高级语言,其官方CPython编译器也是一个极其庞大的项目(用C语言编写)。

当说“用Python写一个Python编译器”时,我们通常指的是以下几种情况,我会从简单到复杂为你介绍:

  1. 最简单的解释器:只支持极少数Python语法,用于教学目的。
  2. 使用解析器生成工具:这是最实用、最接近“真·编译器”的入门方法。
  3. 最真实的挑战:实现一个功能完整的、能将Python编译成机器码的编译器(这是一个博士级别的项目)。

从零开始写一个微型Python解释器

这种方法不依赖任何外部库,能让你深刻理解解释器是如何工作的,我们将实现一个非常简化的版本,只支持整数加减乘除和变量赋值。

核心思想

我们将代码读入一个字符串,然后手动实现一个递归下降解析器 来解析它,最后直接执行解析后的结构。

Python写Python编译器?性能如何实现?-图2
(图片来源网络,侵删)

步骤 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

优点:

Python写Python编译器?性能如何实现?-图3
(图片来源网络,侵删)
  • 完全不依赖外部库,能让你从底层理解解析和执行的原理。
  • 代码量相对较少,易于学习和修改。

缺点:

  • 实现复杂的语法(如 if/else, for 循环)会变得异常困难。
  • 错误处理能力很弱。
  • 性能低下,因为边解析边执行。

使用解析器生成工具(推荐)

这是专业和半专业项目中更常用的方法,我们使用一个工具(如 slyply)来帮我们完成最复杂的部分——语法分析,我们只需要定义好语法规则,工具就能自动生成一个解析器。

我们将使用 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编译器

这是终极目标,也是难度最高的,一个完整的编译器通常包含以下阶段:

  1. 前端:

    • 词法分析: 将源代码转换为Token流。(可以用sly完成)
    • 语法分析: 将Token流转换成抽象语法树,AST是源代码的树状结构表示。(可以用slyast模块)
    • 语义分析: 检查AST的语义是否正确,比如变量是否已声明、类型是否匹配等。
  2. 中端:

    • 中间代码生成: 将AST转换成一种与机器无关的、更简单的中间表示,三地址码。
  3. 后端:

    • 优化: 对中间代码进行优化,使其更高效。
    • 目标代码生成: 将优化后的中间代码转换成特定机器的汇编语言或机器码。

如何实现?

  • 目标: 编译成C代码或汇编代码,然后调用GCC/Clang等外部工具链来生成可执行文件,这是一个非常现实的目标。
  • 挑战:
    • Python的动态性: Python的类型是动态的,这使得编译时优化变得非常困难,你需要实现一个运行时类型系统来支持 isinstance, getattr 等操作。
    • 巨大的标准库: 你的编译器需要知道如何处理Python内置函数和类型。
    • 字节码 vs 机器码: CPython解释器实际上是将Python源码编译成字节码,然后在虚拟机上执行,一个更“简单”的Python编译器可以模仿这个过程,生成自己的字节码和虚拟机。

一个简化版的思路(生成字节码)

你可以模仿CPython,实现一个能生成.pyc字节码文件的编译器,这需要深入理解 Python字节码 的规范。

  1. 使用 sly 解析Python源码,生成AST。
  2. 遍历AST,将其转换为Python字节码指令列表。
  3. 将字节码指令序列化成 .pyc 文件格式。
  4. (可选)实现一个简单的虚拟机来执行你生成的字节码。

这个项目非常庞大,但也是理解Python内部工作原理的最佳途径。

总结与建议

方案 难度 核心工具 适用场景 学习价值
微型解释器 ★☆☆☆☆ 教学,快速原型 ★★★★★ (理解底层原理)
解析器生成工具 ★★★☆☆ sly, ply 构建DSL,小型语言 ★★★★☆ (行业标准实践)
真实编译器 ★★★★★ 自定义 + 外部工具链 学术研究,大型项目 ★★★★★ (终极挑战)

给你的建议:

  1. 从方案一(微型解释器)开始:亲手实现一遍,你会对“解析”和“执行”有最直观的感受。
  2. 然后尝试方案二(sly:对比一下你会发现,sly 让代码结构清晰了无数倍,并且更容易扩展,这是构建任何语言工具的必经之路。
  3. 最后挑战方案三:当你对前两个方案非常熟悉后,可以尝试去阅读CPython的源码,或者研究一下像 mypyc (CPython的AOT编译器) 这样的项目,它们是实现“真·Python编译器”的绝佳参考。

祝你编译器开发之旅愉快!这是一个充满挑战和乐趣的过程。

分享:
扫描分享到社交APP
上一篇
下一篇