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

news/2025/2/1 21:46:47/文章来源:https://www.cnblogs.com/wang_yb/p/18696521

前一篇我们介绍了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

/* 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 -*-

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

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

defff max(x, y):ifaa x >= y:return xelsebb: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

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代码的。

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

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

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

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

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

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/877688.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MyBatis之jdbc属性外部配置

将jdbc数据库连接属性写在db.properties中,如图 然然后在配置文件中引入

25.2.1小记

Object类Object类中自带的toString和equals函数(默认比较管理者是否管理相同的对象,可以通过对子类函数的重构实现正常比较) // @Override//重写,编译器会默认构造类型检查public boolean equals(Object obj) {//向下造型CD cc = (CD)obj;return artist.equals(cc.artist…

“尝试一下挣钱的辛苦”之装师

我尝试了把我做的兽头卖出去,记录一下从孩子出生到找到妈咪领养的过程 因为没有太多预算,所以兽设没有找画师,我自己构思的;整个制作过程都要用到热熔胶,很容易烫到>_<(热熔胶——手作娘离不开的东西),梳理毛布也很让人头疼,弄得房间里都是毛毛,虽然但是,孩子…

“尝试一下挣钱的辛苦”

我尝试了把我做的兽头卖出去,记录一下从孩子出生到找到妈咪领养的过程 因为没有太多预算,所以兽设没有找画师,我自己构思的;整个制作过程都要用到热熔胶,很容易烫到>_<(热熔胶——手作娘离不开的东西),梳理毛布也很让人头疼,弄得房间里都是毛毛,虽然但是,孩子…

VScode使用插件open-in-browser在默认浏览器中打开html文件

1. vscode extension中搜索open in browser,并下载。 我下载的第一个2. 打开.html文件 alt+shift+b手动选择特定浏览器打开 alt+b用默认浏览器打开,如果没有设置默认浏览器,可能出现以下问题3. 配置默认浏览器 ctrl+shift+p打开command palette 输入settings.json,打开Open…

在MacOS上安装sqllite

参考教程 https://www.runoob.com/sqlite/sqlite-installation.html 1.下载sqllite安装包 https://www.sqlite.org/2025/sqlite-autoconf-3480000.tar.gz wget https://www.sqlite.org/2025/sqlite-autoconf-3480000.tar.gz tar -xvzf sqlite-autoconf-3480000.tar.gz cd sqlit…

【译】MongoDB EF Core 提供程序:有什么新功能?

原文 | Rishit, Luce 翻译 | 郑子铭 这是 Rishit Bhatia 和 Luce Carter 的客座文章。Rishit 是 MongoDB 的高级产品经理,专注于 .NET 开发人员体验,在进入产品管理部门之前,他已经使用 C# 工作多年。Luce 是 MongoDB 的开发倡导者、Microsoft MVP,热爱代码、阳光和学习。本…

06. 文件权限

一、文件属性Linux 系统是一个典型的多用户系统,不同的用户处于不同的地位,拥有不同的权限。为了保证系统的安全性,Linux 系统对不同的访问用户访问同一个文件(包括目录文件)的权限做了不同的规定。在 Linux 中,我们可以使用 ll 或者 ls -l 命令来显示一个文件的属性以及…

java中的Hashtable

Hashtable Hashtable 是 Java 中的一个古老的集合类,它实现了 Map 接口,基于哈希表存储键值对。 Hashtable 是线程安全的,所有方法都用 synchronized 修饰,因此在多线程环境下可以安全使用。 不过,由于它的性能较差,现代开发中更推荐使用 HashMap Hashtable中的t是小写,…

[HCTF 2018]admin

打开靶机进入登录界面,账号输入admin,密码随便输入一个密码,使用burp抓包后发送到攻击器将密码设为变量,导入字典,开始攻击发现密码"123"的返回长度不同,使用"123"登录,得到flag

DeepSeek LLM

一、背景动机开源社区的关注点:LLaMA 之后,开源社区主要关注训练固定规模的高质量 LLM(如 7B、13B、34B 和 70B),而对 LLM 的缩放定律研究探索较少。缩放定律的重要性:当前开源 LLM 仍处于 AGI 发展的初期阶段,因此研究扩展定律对于未来发展至关重要。缩放结论的分歧:早…