『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.h和Parser/tokenizer.c。
词法分析阶段,将我们的Python源代码转换为一系列由CPython定义的Token流。
Token的定义可参考:Parser/token.c
/* Token names */
const char * const _PyParser_TokenNames[] = {
"ENDMARKER",
"NAME",
"NUMBER",
"STRING",
"NEWLINE",
"INDENT",
"DEDENT",
"LPAR",
"RPAR",
// 省略... ...
"NL",
"<ERRORTOKEN>",
"<ENCODING>",
"<N_TOKENS>",
};
下面我们写一段简单的代码,然后看看词法分析后生成的是什么,直观的来了解下词法分析的结果。
def max(x, y):
if x >= y:
return x
else:
return y
这是一个很简单的函数max,就是从x, y两个参数中选择一个大的返回。
查看词法分析的结果,在命令行中执行如下命令:
$ 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 -*-。
此外,词法分析只是简单的解析源码,并转换为CPython中Token,它并不管代码的语法是否正确。
比如,我把上面的Python代码改为:
defff max(x, y):
ifaa x >= y:
return x
elsebb:
return y
这里面的关键字def改成defff,if改成ifaa,else改成了elsebb,明显这是错误的Python代码,但是不影响词法分析。

依然可以正常的词法分析并生成Token。
2.2. 语法分析
语法分析的工作首先是检查上一步生成的输入Token流是否是语法正确的Python代码。
比如上一节中最后的那段错误的Python代码,虽然可以进行词法分析,但是在语法分析阶段生成AST的时候会报错。
下图就是生成AST的时候,提示了语法错误,并且无法生成AST。
生成AST的命令:python.exe -m ast <file>

语法分析的过程远比词法分析复杂很多很多,CPython中的语法分析代码请参考:Parser/parser.c
把语法错误改成最初的正确语法之后,再次生成AST:
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文件使用如下的命令:
$ 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),上一篇中介绍了代码对象。
只是这个文件是二进制的,无法直接打开查看,想看字节码的话,可以用如下的命令:
$ 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代码的。
具体的编译过程和优化过程并没有详细说明,这需要对编译原理有深入的认识,而且限于自己的能力,我也无法通过一篇文章就说明清楚。
感兴趣的朋友可以研究研究github上CPython的源码,本文参考的源码是CPython 3.12分支。
最后,总结一下本文的主要内容。
首先,CPython编译器的架构沿袭了传统的设计理念,其主要组成部分包括前端和后端。
前端通常被称为解析器,其核心职责是将源代码转换为抽象语法树(Abstract Syntax Tree, AST)。
这一过程主要包括词法分析和语法分析,词法分析负责从输入的文本中生成一系列具有语言意义的基本单元,即标记(Tokens)。
语法分析主要生成解析树以及将其转换为AST。
后端,有时也被称作编译器,接收前端生成的AST作为输入,据此生成代码对象,并进行优化处理。
最终,生成的代码对象即可用于后续执行。
『Python底层原理』--CPython如何编译代码的更多相关文章
- 『Python基础-1 』 编程语言Python的基础背景知识
#『Python基础-1 』 编程语言Python的基础背景知识 目录: 1.编程语言 1.1 什么是编程语言 1.2 编程语言的种类 1.3 常见的编程语言 1.4 编译型语言和解释型语言的对比 2 ...
- 『Python基础-12』各种推导式(列表推导式、字典推导式、集合推导式)
# 『Python基础-12』各种推导式(列表推导式.字典推导式.集合推导式) 推导式comprehensions(又称解析式),是Python的一种独有特性.推导式是可以从一个数据序列构建另一个新的 ...
- 『Python基础-11』集合 (set)
# 『Python基础-11』集合 (set) 目录: 集合的基本知识 集合的创建 访问集合里的值 向集合set增加元素 移除集合中的元素 集合set的运算 1. 集合的基本知识 集合(set)是一个 ...
- 『Python基础-10』字典
# 『Python基础-10』字典 目录: 1.字典基本概念 2.字典键(key)的特性 3.字典的创建 4-7.字典的增删改查 8.遍历字典 1. 字典的基本概念 字典一种key - value 的 ...
- 『Python基础-9』元祖 (tuple)
『Python基础-9』元祖 (tuple) 目录: 元祖的基本概念 创建元祖 将列表转化为元组 查询元组 更新元组 删除元组 1. 元祖的基本概念 元祖可以理解为,不可变的列表 元祖使用小括号括起所 ...
- 『Python基础-8』列表
『Python基础-8』列表 1. 列表的基本概念 列表让你能够在一个地方存储成组的信息,其中可以只包含几个 元素,也可以包含数百万个元素. 列表由一系列按特定顺序排列的元素组成.你可以创建包含字母表 ...
- 『Python基础-7』for循环 & while循环
『Python基础-7』for循环 & while循环 目录: 循环语句 for循环 while循环 循环的控制语句: break,continue,pass for...else 和 whi ...
- 『Python基础-6』if语句, if-else语句
# 『Python基础-6』if语句, if-else语句 目录: 条件测试 if语句 if-else语句 1. 条件测试 每条if语句的核心都是一个值为True或False的表达式,这种表达式被称为 ...
- 『Python基础-5』数字,运算,转换
『Python基础-5』数字,运算,转换 目录 基本的数字类型 二进制,八进制,十六进制 数字类型间的转换 数字运算 1. 数字类型 Python 数字数据类型用于存储数学上的值,比如整数.浮点数.复 ...
- 『Python基础-4』字符串
# 『Python基础-4』字符串 目录 1.什么是字符串 2.修改字符串 2.1 修改字符串大小 2.2 合并(拼接)字符串 2.3 使用乘号'*'来实现字符串的叠加效果. 2.4 在字符串中添加空 ...
随机推荐
- win10中Docker安装、构建镜像、创建容器、Vscode连接实例
Docker方便一键构建项目所需的运行环境:首先构建镜像(Image).然后镜像实例化成为容器(Container),构成项目的运行环境.最后Vscode连接容器,方便我们在本地进行开发.下面以一个简 ...
- 数据抽取平台pydatax使用案例---11个库项目使用
数据抽取平台pydatax,前期项目做过介绍: 1,数据抽取平台pydatax介绍--实现和项目使用 项目2: 客户有9个分公司,用的ERP有9套,有9个库,不同版本,抽取的同一个表字段长度有不一样, ...
- NET 6 中新增的LINQ 方法
.NET 6 中添加了许多 LINQ 方法. 下表中列出的大多数新方法在 System.Linq.Queryable 类型中具有等效方法. 欢迎关注 如果你刻意练习某件事情请超过10000小时,那么你 ...
- Git for windows下Filename too long
前情 Git(读音为/gɪt/)是一个开源的分布式版本控制系统,可以有效.高速地处理从很小到非常大的项目版本管理,我公司目前都是基于Git来管理项目代码的. 坑位 最近在拉取代码时报如下错误,其中有句 ...
- ksmbd 条件竞争漏洞挖掘:思路与案例
ksmbd 条件竞争漏洞挖掘:思路与案例 ksmbd 条件竞争漏洞挖掘:思路与案例.drawio 本文介绍从代码审计的角度分析.挖掘条件竞争.UAF 漏洞思路,并以 ksmbd 为实例介绍审计的过程和 ...
- 鸿蒙UI开发快速入门 —— part02: 组件开发
1. 组件基本介绍 在ArkUI中,UI显示的内容均为组件,由框架直接提供的称为系统组件,由开发者定义的称为自定义组件.在进行 UI 界面开发时,通常不是简单的将系统组件进行组合使用,而是需要考虑代码 ...
- 基于 .NET 的 Nuget 发版工具
背景 由于 Natasha 及周边项目发版任务多,文件结构也不简单,之前一直使用基于 Github 管道脚本和 XUnit 来发版.这个方案对于发版环境与条件依赖性较强,且不够灵活,因此萌生出做一个本 ...
- Linux下TCP/IP编程--TCP实战
之前尝试过windows下的简单TCP客户端服务器编写,这次尝试下一下Linux环境下的TCP 客户端代码 #include <stdio.h> #include <stdlib.h ...
- 【JavaWeb】前后端分离SpringBoot项目快速排错指南
1 发起业务请求 打开浏览器开发者工具,同时显示网络(Internet)和控制台(console) 接着,清空控制台和网络的内容,如下图 然后,点击你的业务按钮,发起请求. 首先看控制台有没有报错信息 ...
- 【Rive】波动文字
1 前言 本文将使用文本修改器(Text Modifiers)做文字动画,实现文字波动效果. 按以下步骤可以创建一个 Modifier Group 和 Range. 部分参数的释义如下. ...