『Python底层原理』--CPython如何编译代码

前一篇我们介绍了CPython VM的运行机制,它基于一系列字节码指令来实现程序逻辑。

不过,Python字节码在完整描述代码功能上存在局限性,于是代码对象 应运而生。像模块、函数这类代码块的执行,本质上就是对应代码对象 的运行,代码对象涵盖了字节码、常量、变量名以及各类属性信息。

实际开发Python程序时,编写的是常规Python代码,而非字节码或直接创建代码对象。

这就需要CPython编译器发挥作用,将源代码转换为代码对象。

本篇中,我们将探究CPython编译器的工作流程,尝试解析其如何完成编译的任务,从而理解Python程序的底层执行逻辑。

1. 编译器概述

从广义上看,编译器 是就是一个程序,负责将源代码 从一种编程语言转换成另一种语言。

编译器种类繁多,但在多数情况下,通常所指的编译器是静态编译器,这类工具专门用于将高级编程语言编写的程序转换为可以直接被计算机硬件执行的机器码。

传统的编译器如下图所示,一般分为三个部分:前端,优化器,后端。

编译器的前端 负责将源代码转换为一种中间表示(Intermediate Representation, IR)。

随后,优化器 接收该IR,执行一系列优化操作,并将优化后的IR传递至负责生成目标机器代码的后端

这里为什么不直接将源代码 编译成机器码 ,而是采用这种前端->优化器->后端的三阶段设计呢?

其中还要多设计一种中间语言IR,是否多此一举呢?

其实编译器采用这种架构有显著的优势 ,其中中间语言IR设计得既不依赖于特定的源语言也不绑定于具体的目标架构,当编译器需要支持新的编程语言时,仅需开发相应的前端模块;

当编译器扩展对新型目标硬件的支持,只需增加对应的后端模块即可。

这样不仅提升了编译系统的灵活性,还极大地简化了其维护与升级过程。

CPython编译器也是采用的这种三阶段设计,只不过,它的编译器前端针对的是Python源码,中间代码是抽象语法树AST),最后生成的不是直接针对硬件的机器码,而是代码对象Code Object)。

2. 编译器关键组件

接下来,来看看CPython编译器中的关键组件,它们是完成从Python源码代码对象的核心部分。

扩展上一节中的图,将编译器中的组件加入其中。

图中关键的组件是词法分析 (拆分源码,生成Token),语法分析 (从Token生成AST)以及编译 (从AST代码对象 CodeObject)三个部分。

2.1. 词法分析

这个步骤中,编译器将源代码 拆分为有意义的标记Token(如标识符、关键字、运算符等),方便后续的语法分析处理。

词法分析 在英文中成为tokenizer,它在CPython源码中的位置:Parser/tokenizer.hParser/tokenizer.c

词法分析 阶段,将我们的Python源代码转换为一系列由CPython定义的Token流。

Token的定义可参考:Parser/token.c

c 复制代码
/* Token names */

const char * const _PyParser_TokenNames[] = {
    "ENDMARKER",
    "NAME",
    "NUMBER",
    "STRING",
    "NEWLINE",
    "INDENT",
    "DEDENT",
    "LPAR",
    "RPAR",
    // 省略... ...
    "NL",
    "<ERRORTOKEN>",
    "<ENCODING>",
    "<N_TOKENS>",
};

下面我们写一段简单的代码,然后看看词法分析 后生成的是什么,直观的来了解下词法分析的结果。

python 复制代码
def max(x, y):
    if x >= y:
        return x
    else:
        return y

这是一个很简单的函数max,就是从x, y两个参数中选择一个大的返回。

查看词法分析的结果,在命令行中执行如下命令:

bash 复制代码
$  python.exe -m tokenize .\cpython-compiler.py
0,0-0,0:            ENCODING       'utf-8'
1,0-1,3:            NAME           'def'
1,4-1,7:            NAME           'max'
1,7-1,8:            OP             '('
1,8-1,9:            NAME           'x'
1,9-1,10:           OP             ','
1,11-1,12:          NAME           'y'
1,12-1,13:          OP             ')'
1,13-1,14:          OP             ':'
1,14-1,15:          NEWLINE        '\n'
2,0-2,4:            INDENT         '    '
2,4-2,6:            NAME           'if'
2,7-2,8:            NAME           'x'
2,9-2,11:           OP             '>='
2,12-2,13:          NAME           'y'
2,13-2,14:          OP             ':'
2,14-2,15:          NEWLINE        '\n'
3,0-3,8:            INDENT         '        '
3,8-3,14:           NAME           'return'
3,15-3,16:          NAME           'x'
3,16-3,17:          NEWLINE        '\n'
4,4-4,4:            DEDENT         ''
4,4-4,8:            NAME           'else'
4,8-4,9:            OP             ':'
4,9-4,10:           NEWLINE        '\n'
5,0-5,8:            INDENT         '        '
5,8-5,14:           NAME           'return'
5,15-5,16:          NAME           'y'
5,16-5,17:          NEWLINE        '\n'
6,0-6,0:            DEDENT         ''
6,0-6,0:            DEDENT         ''
6,0-6,0:            ENDMARKER      ''

其中,cpython-compiler.py文件中就是上面max函数的代码。

从上面可以看出,CPython在第一行自动为我们添加了utf-8的说明,也就是说,如果你使用的是Python3,

那么,不需要像以前Python2时那样,在代码第一行指定# -*- coding: utf-8 -*-

此外,词法分析 只是简单的解析源码,并转换为CPythonToken,它并不管代码的语法是否正确。

比如,我把上面的Python代码改为:

python 复制代码
defff max(x, y):
    ifaa x >= y:
        return x
    elsebb:
        return y

这里面的关键字def改成defffif改成ifaaelse改成了elsebb,明显这是错误的Python代码,但是不影响词法分析

依然可以正常的词法分析并生成Token

2.2. 语法分析

语法分析 的工作首先是检查上一步生成的输入Token流是否是语法正确的Python代码。

比如上一节中最后的那段错误的Python代码,虽然可以进行词法分析 ,但是在语法分析 阶段生成AST的时候会报错。

下图就是生成AST的时候,提示了语法错误,并且无法生成AST

生成AST的命令:python.exe -m ast <file>

语法分析 的过程远比词法分析 复杂很多很多,CPython中的语法分析代码请参考:Parser/parser.c

把语法错误改成最初的正确语法之后,再次生成AST

python 复制代码
def max(x, y):
    if x >= y:
        return x
    else:
        return y

这样就将代码变成了一棵抽象语法树AST)。画成示意图大致如下:

语法分析之后,得到了AST,也就是CPython编译器中间代码IR),

接下来经过CPython编译器 的优化之后生成优化的AST,最后进入后端处理。

2.3. 编译

编译CPython编译器 3个关键组件中的最后一个,经过编译之后,将生成字节码 ,保存在.pyc文件中。

再次提醒,CPython编译器 和传统静态语言(C/C++Rust等)的编译器不一样,它生成的不是针对特定硬件平台的机器码。

我们运行Python程序时,实际是由Python解释器逐条执行编译之后生成的字节码。

编译Python文件使用如下的命令:

bash 复制代码
$  python.exe -m compileall .\cpython-compiler.py
Compiling '.\\cpython-compiler.py'...

$  ls .\__pycache__\

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2025/02/01  21:16:14            248 cpython-compiler.cpython-312.pyc

执行命令之后,可以看到生成了一个__pycache__文件夹,其中有编译之后的字节码文件,即.pyc文件。

编译 相关的CPython源码请参考:Python/compile.c

编译 之后生成的.pyc文件中的字节码其实就是代码对象CodeObject),上一篇中介绍了代码对象

只是这个文件是二进制的,无法直接打开查看,想看字节码的话,可以用如下的命令:

bash 复制代码
$  python.exe -m dis .\cpython-compiler.py
  0           0 RESUME                   0

  1           2 LOAD_CONST               0 (<code object max at 0x00000207FC2ADB50, file ".\cpython-compiler.py", line 1>)
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (max)
              8 RETURN_CONST             1 (None)

Disassembly of <code object max at 0x00000207FC2ADB50, file ".\cpython-compiler.py", line 1>:
  1           0 RESUME                   0

  2           2 LOAD_FAST                0 (x)
              4 LOAD_FAST                1 (y)
              6 COMPARE_OP              92 (>=)
             10 POP_JUMP_IF_FALSE        2 (to 16)

  3          12 LOAD_FAST                0 (x)
             14 RETURN_VALUE

  5     >>   16 LOAD_FAST                1 (y)
             18 RETURN_VALUE

3. 总结

本篇主要从比较宏观的角度介绍了CPython如何编译Python代码的。

具体的编译过程优化过程并没有详细说明,这需要对编译原理有深入的认识,而且限于自己的能力,我也无法通过一篇文章就说明清楚。

感兴趣的朋友可以研究研究githubCPython的源码,本文参考的源码是CPython 3.12分支

最后,总结一下本文的主要内容。

首先,CPython编译器 的架构沿袭了传统的设计理念,其主要组成部分包括前端后端

前端 通常被称为解析器 ,其核心职责是将源代码转换为抽象语法树(Abstract Syntax Tree, AST)。

这一过程主要包括词法分析语法分析词法分析负责从输入的文本中生成一系列具有语言意义的基本单元,即标记(Tokens)。

语法分析 主要生成解析树以及将其转换为AST

后端 ,有时也被称作编译器 ,接收前端 生成的AST作为输入,据此生成代码对象,并进行优化处理。

最终,生成的代码对象即可用于后续执行。

相关推荐
IT古董1 小时前
【漫话机器学习系列】072.异常处理(Handling Outliers)
人工智能·python·机器学习
EelBarb2 小时前
YOLO11/ultralytics:环境搭建
python·yolo·ultralytics
加德霍克3 小时前
【机器学习】自定义数据集使用scikit-learn中的包实现线性回归方法对其进行拟合
python·机器学习·线性回归·scikit-learn·作业
西猫雷婶4 小时前
python学opencv|读取图像(五十二)使用cv.matchTemplate()函数实现最佳图像匹配
人工智能·python·opencv·计算机视觉
Nerous_5 小时前
【Python】 使用pygame库实现新年烟花
开发语言·python·pygame
Zda天天爱打卡5 小时前
【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】1.28 存储之道:跨平台数据持久化方案
开发语言·python·numpy
Music 爱好者5 小时前
DRF开发避坑指南01
数据库·python·django·sqlite
Melancholy 啊5 小时前
细说机器学习算法之ROC曲线用于模型评估
人工智能·python·算法·机器学习·数据挖掘
爱码小白5 小时前
InnoSetup使用教程笔记
笔记·python