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

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

比如,我把上面的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

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

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

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

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

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

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

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

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

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

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

『Python底层原理』--CPython如何编译代码的更多相关文章

  1. 『Python基础-1 』 编程语言Python的基础背景知识

    #『Python基础-1 』 编程语言Python的基础背景知识 目录: 1.编程语言 1.1 什么是编程语言 1.2 编程语言的种类 1.3 常见的编程语言 1.4 编译型语言和解释型语言的对比 2 ...

  2. 『Python基础-12』各种推导式(列表推导式、字典推导式、集合推导式)

    # 『Python基础-12』各种推导式(列表推导式.字典推导式.集合推导式) 推导式comprehensions(又称解析式),是Python的一种独有特性.推导式是可以从一个数据序列构建另一个新的 ...

  3. 『Python基础-11』集合 (set)

    # 『Python基础-11』集合 (set) 目录: 集合的基本知识 集合的创建 访问集合里的值 向集合set增加元素 移除集合中的元素 集合set的运算 1. 集合的基本知识 集合(set)是一个 ...

  4. 『Python基础-10』字典

    # 『Python基础-10』字典 目录: 1.字典基本概念 2.字典键(key)的特性 3.字典的创建 4-7.字典的增删改查 8.遍历字典 1. 字典的基本概念 字典一种key - value 的 ...

  5. 『Python基础-9』元祖 (tuple)

    『Python基础-9』元祖 (tuple) 目录: 元祖的基本概念 创建元祖 将列表转化为元组 查询元组 更新元组 删除元组 1. 元祖的基本概念 元祖可以理解为,不可变的列表 元祖使用小括号括起所 ...

  6. 『Python基础-8』列表

    『Python基础-8』列表 1. 列表的基本概念 列表让你能够在一个地方存储成组的信息,其中可以只包含几个 元素,也可以包含数百万个元素. 列表由一系列按特定顺序排列的元素组成.你可以创建包含字母表 ...

  7. 『Python基础-7』for循环 & while循环

    『Python基础-7』for循环 & while循环 目录: 循环语句 for循环 while循环 循环的控制语句: break,continue,pass for...else 和 whi ...

  8. 『Python基础-6』if语句, if-else语句

    # 『Python基础-6』if语句, if-else语句 目录: 条件测试 if语句 if-else语句 1. 条件测试 每条if语句的核心都是一个值为True或False的表达式,这种表达式被称为 ...

  9. 『Python基础-5』数字,运算,转换

    『Python基础-5』数字,运算,转换 目录 基本的数字类型 二进制,八进制,十六进制 数字类型间的转换 数字运算 1. 数字类型 Python 数字数据类型用于存储数学上的值,比如整数.浮点数.复 ...

  10. 『Python基础-4』字符串

    # 『Python基础-4』字符串 目录 1.什么是字符串 2.修改字符串 2.1 修改字符串大小 2.2 合并(拼接)字符串 2.3 使用乘号'*'来实现字符串的叠加效果. 2.4 在字符串中添加空 ...

随机推荐

  1. java三次大作业的全面总结

    一:前言 知识点总结: 数据结构:题目涉及到了字典或哈希表用于存储题目信息.试卷信息和学生信息:列表用于存储多个题目.试卷和学生的集合:对象用于封装题目.试卷.学生和答案的具体属性. 字符串解析:题目 ...

  2. 开源IDS/IPS Suricata的部署与使用

    目录 前言 在Linux上部署Suricata Suricata的基本配置 配置文件 Suricata的规则 Suricata的使用 Suricata检测SQL注入 前言 Suricata 是一个高性 ...

  3. JAVA并发编程学习笔记之CLH队列锁

    NUMA与SMP SMP(Symmetric Multi-Processor),即对称多处理器结构,指服务器中多个CPU对称工作,每个CPU访问内存地址所需时间相同.其主要特征是共享,包含对CPU,内 ...

  4. px转换为rem,响应式js

    (function (doc, win) { var docEl = doc.documentElement, resizeEvt = 'orientationchange' in window ? ...

  5. YAML语法基础

    YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言). YAML 的语法和其他高级语言类似,并且可以简单表达清单.散列表,标量等数 ...

  6. Windows 禁用笔记本键盘

    背景 笔记本键盘+机械键盘组合如下图: 由此产生一个问题: 笔记本键盘现在的用处是什么? 没什么用,那我们何不把桌面的位置利用起来? 这样怎么样? ===> 为了防止放东西时候误触,我们需要把笔 ...

  7. Sealos Devbox 基础教程:使用 Cursor 从零开发一个完整的项目

    作者:熊猫Jay,上市公司技术负责人,破局AI 提示词.AI编程教练.通往AGI之路内容共创者 最近发现身边越来越多人尝试用 Cursor 写代码.开发小产品了. 如果想要实现商业化或引流,我们的小工 ...

  8. Winform在主窗体加载前弹出登录窗体

    1:主窗体代码 点击查看代码 //实例化登录窗体 FrmLogin frmLogin = new FrmLogin(); //读取登录窗体的返回结果 DialogResult dialogResult ...

  9. 鸿蒙UI开发快速入门 —— part03: 组件的生命周期

    1. 什么是组件的生命周期 组件的生命周期是我们开发一个组件必须要关注的内容,组件的生命周期,指的是组件的创建.渲染.销毁等过程.因为这个过程就类似于人从出生到离世的过程,从而称为:组件的生命周期. ...

  10. django视图层与cbv源码分析

    目录 一.视图层之必会三板斧 二.JsonResponse对象 两种序列化数据的方式 方式一:使用json模块 方式二:使用JsonResponse对象 使用JsonResponse对象序列化除字典外 ...