前一篇我们介绍了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 xelse: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 xelsebb: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 xelse: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.py0 0 RESUME 01 2 LOAD_CONST 0 (<code object max at 0x00000207FC2ADB50, file ".\cpython-compiler.py", line 1>)4 MAKE_FUNCTION 06 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 02 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_VALUE5 >> 16 LOAD_FAST 1 (y)18 RETURN_VALUE
3. 总结
本篇主要从比较宏观的角度介绍了CPython
如何编译Python
代码的。
具体的编译过程和优化过程并没有详细说明,这需要对编译原理有深入的认识,而且限于自己的能力,我也无法通过一篇文章就说明清楚。
感兴趣的朋友可以研究研究github
上CPython
的源码,本文参考的源码是CPython 3.12分支。
最后,总结一下本文的主要内容。
首先,CPython编译器的架构沿袭了传统的设计理念,其主要组成部分包括前端和后端。
前端通常被称为解析器,其核心职责是将源代码转换为抽象语法树(Abstract Syntax Tree, AST
)。
这一过程主要包括词法分析和语法分析,词法分析负责从输入的文本中生成一系列具有语言意义的基本单元,即标记(Tokens)。
语法分析主要生成解析树以及将其转换为AST
。
后端,有时也被称作编译器,接收前端生成的AST
作为输入,据此生成代码对象,并进行优化处理。
最终,生成的代码对象即可用于后续执行。