CPython开发实战:添加自增(++)和自减(--)运算符

相信很多人刚开始学习Python的时候都有这样的疑问,为什么Python不像C语言那样有自增(++)和自减(--)运算符?

如果网上搜索这个问题可能会得到不同的答案,比如Python的整型是不可变对象,比如抽象度高不需要考虑机器指令(INC、DEC),再比如自增和自减容易对阅读代码造成误解。总之,这些答案或官方或民间给出了Python不能这样做的理由。

实际上,通过魔改CPython的源码还是可以实现自增和自减运算符。本篇文章就带你一步步实现一个支持自增和自减的Python。

本篇文章属于《CPython开发实战》系列。不同于之前的文章,本次实战涉及到Python字节码的修改,建议提前阅读之前的文章------CPython开发实战:添加loop语法CPython开发实战:魔改lambda函数(一)CPython开发实战:实现None感知运算符?.和??

在实战开始前需要准备开发环境。由于是基于Windows平台开发的,在实战之前需要准备以下几个工具------

  1. GIT:用来下载cpython依赖,比如sqlite、bzip、zlib等

  2. MSVC:用来编译cpython项目,可以直接下载Visual Studio 2022,也可以下载Build Tools

  3. 低版本的Python:用来生成部分编译文件

  4. Visual Studio 2022(可选):用来debug,打断点

另外本次实战采用了GitHub上当前最新的cpython分支,Python 3.13.0 alpha2版本。

1. 在命令行中运行PCBuild/build.bat编译cpython

第一次克隆下来的cpython需要执行这条命令,确保在本地环境可以编译通过。编译中需要用到git、MSVC和旧版本的Python,并且需要保持网络通畅。

2. 在Grammar/Token文件中第46行添加如下代码

arduino 复制代码
DOUBLEPLUS              '++'
DOUBLEMIN               '--'

该文件是记录了Python语法中所有的Token,我们会用脚本读取这个文件生成词法分析器。由于本次实战是给Python添加自增和自减运算符,因此需要在这个文件中添加这两个Token,并命名为DOUBLEPLUSDOUBLEMIN

3. 在Parser/Python.asdl文件中第83行添加如下代码

scss 复制代码
         | Inc(expr value)
         | Dec(expr value)

asdl文件描述了python的抽象语法树(AST)的结构。像其他语言一样,Python也是通过AST来组织语法结构的。通过解析源码生成AST树,cpython可以更方便的用递归遍历多叉树的方式访问每个语法节点,从而生成字节码。

由于自增和自减运算属于表达式,所以在expr父节点下插入这两个节点。

4. 在Grammar/python.gram文件中第821行添加如下代码

ini 复制代码
    | a=primary '++' { _PyAST_Inc(a, EXTRA) }
    | a=primary '--' { _PyAST_Dec(a, EXTRA) }

gram文件描述了Python的语法。和asdl文件一样,它也会被脚本读取生成cpython的语法分析器。和asdl不同在于它关注Python源码是否有语法错误,比如判断try语句是否有catch等。

在整个Python语法中,atom是最小不可分语法元素,也是优先级最高的语法元素,比如NAMEtoken、True对象、False对象和None对象等。比它稍微弱一点的就是primary语法元素,比如切片运算([])、取属性运算(.)等。

本次实战的自增(++)和自减(--)运算也被设计到primary范畴,说明它有非常高的优先级。因此,和数学运算、位运算、与或非运算相比,自增自减运算更高。

5. 在命令行中运行PCBuild/build.bat --regen生成词法分析程序

更新代码后需要运行脚本生成对应的词法分析器,该词法分析器会融入cpython项目流程参与Python代码的编译。

运行git status可以看到涉及词法分析的文件都被修改了。

6. 在命令行中运行PCBuild/build.bat生成Python程序

将上一步生成的词法分析器代码和其他剩余代码一起编译,生成新的Python解释器。这样我们通过一个脚本尝试运行一下来验证之前的步骤。

python 复制代码
a = 5
a ++
print(a)

然后运行python.exe -m ast test.py,通过ast模块解析该代码的AST。如果生成的结果如下说明是成功的:

7. 在Python/symtable.c文件中第2152行添加如下代码

c 复制代码
    case Inc_kind:
        VISIT(st, expr, e->v.Inc.value);
        break;
    case Dec_kind:
        VISIT(st, expr, e->v.Dec.value);
        break;

在生成AST树后,cpython会遍历各个节点生成符号表。

由于自增和自减表达式只包含一个value节点,所以只需要VISIT它的value。除此以外,没有任何操作。

8. 在Python/compile.c文件中第6295行添加如下代码

c 复制代码
    case Inc_kind:
        VISIT(c, expr, e->v.Inc.value);
        ADDOP(c, loc, INC);
        break;
    case Dec_kind:
        VISIT(c, expr, e->v.Inc.value);
        ADDOP(c, loc, DEC);
        break;

Python字节码是在第二次遍历AST树过程中生成的。当遍历到自增或自减节点时,我们应该先VISIT该节点的value,然后发射INCDEC字节码。注意,这个两个字节码是我们等会要在后续步骤中添加的字节码,它们分别对栈顶元素做加一和减一操作。而由于在发射这两个字节码之前会先遍历value的内容,所以栈顶一定会有一个元素。

9. 在Python/bytecodes.c文件第1073行添加如下代码

c 复制代码
        inst(INC, (value1 --  value2 )) {
            assert(PyLong_Check(value1) != NULL);
            value2 = PyLong_FromLong(PyLong_AsLong(value1) + 1L);
        }

        inst(DEC, (value1 --  value2 )) {
            assert(PyLong_Check(value1) != NULL);
            value2 = PyLong_FromLong(PyLong_AsLong(value1) - 1L);
        }

虽然这是一个c文件,但是它本质和前文的asdl、gram文件一样,并不会参与cpython的编译。在cpython项目中,位于Tools/cases_generatorgenerate_cases.py脚本会读取这个c文件生成Python/generated_cases.c.h文件。该文件会直接以include的形式引入到Python虚拟机的核心文件ceval.c文件内。

由于cop-and-patch JIT的唐突引入,generate_cases.py文件可能被拆分成不同的几个脚本,从而对应生成对应不同tire的字节码。如果需要生成Python/generated_cases.c.h的话可以执行PCBuild/build.bat --regen

Python/bytecodes.c文件格式如下------

lua 复制代码
字节码指令类别(字节码指令名称, ( 输入 -- 输出 )) {
    字节码指令执行逻辑
}
  • 字节码指令类别将字节码划分为如下几个范畴
  1. inst:最基本的字节码指令,可以消费栈顶的零个或多个项目,也可以生产零个或多个项目到栈顶。如果既不消费也不生产项目说明是过度指令,比如Python3.13的EXTENDED_ARGS字节码指令。
  2. op:也是最基本的字节码指令,与inst不同地方是它可以作为macro的子字节码指令,比如_TO_BOOL
  3. macro:符合字节码指令,可以使用op的字节码指令组合,比如END_FOR
  • 字节码指令名称是以非Python和C保留字的ASCII大写字母表示的,比如LOAD_CONSTSTORE_NAME等。
  • 输入和输出是指字节码指令的副作用(side-effect),其中输入指需要消耗的栈顶的项目,输出是需要压入栈顶的项目。
  • 字节码执行逻辑即利用输入和输出的操作逻辑,直接用C语言描绘。

举个例子,比如如果这样写bytecodes.c文件------

c 复制代码
    inst ( STORE_FAST, (value --) ) {
        SETLOCAL(oparg, value);
    }

它会在generated_cases.c.h文件中生成如下代码------

c 复制代码
    TARGET(STORE_FAST) {
        PyObject *value = PEEK(1);
        SETLOCAL(oparg, value);
        STACK_SHRINK(1);
        DISPATCH();
    }

其中TARGET宏和DISPATCH宏本质是case语句和goto语句,其定义如下

arduino 复制代码
#define TARGET(OP) case OP
#define DISPATCH goto dispatch

所以STORE_FAST这个字节码指令就是获取栈顶项目,然后将其快速存入符号表队列,并减少队列的长度。

除了以上三类字节码指令类别,cpython还提供两个指令"宏"------

  1. family:这个"宏"将相似的字节码指令归为一个族,方便开发。比如------
scss 复制代码
family(load_attr) = { LOAD_ATTR, LOAD_ATTR_INSTANCE_VALUE, LOAD_SLOT };
  1. pseudo:和family类似,它将字节码指令细化成具体的某些指令。比如------
scss 复制代码
pseudo(JUMP) = { JUMP_FORWARD, JUMP_BACKWARD };

10. 在命令行中运行PCBuild/build.bat --regen生成generated_cases.c.h代码

如果不出意外,项目应该可以成功编译。这时候运行git status可以看到脚本修改了很多文件。

11. 删除旧.pyc文件

这时候如果直接生成Python程序的话可能启动不起来,因为Python启动的时候会预加载内置库的缓存。我们需要在bash下运行如下命令删除内置库的缓存,让它重新生成。

sh 复制代码
find . -name '*.py[co]' -exec rm -f '{}' +

12. 在命令行中运行PCBuild/build.bat --regen生成Python程序

生成好Python程序后可以用第六步的代码检测一下。执行python -m dis .\PCbuild\amd64\test.py可以看到成功生成了INC字节码。

然后直接使用++运算符可以看到5自增成为6了。

本次实战就在此告一段落了。相比之前的实战,它经历了完整的cpython前端和后端流程。结合之前的文章,相信读者对整个cpython项目有了深刻的理解。

相关推荐
A 八方28 分钟前
Python MongoDB
开发语言·python·mongodb
sz66cm2 小时前
Python基础 -- 使用Python实现ssh终端并实现数据处理与统计功能
开发语言·python·ssh
ac-er88884 小时前
如何在Flask中实现国际化和本地化
后端·python·flask
Adolf_19934 小时前
Flask-WTF的使用
后端·python·flask
空城皆是旧梦4 小时前
python爬虫初体验(一)
爬虫·python
藓类少女4 小时前
正则表达式
数据库·python·mysql·正则表达式
深蓝海拓5 小时前
迭代器和生成器的学习笔记
笔记·python·学习
985小水博一枚呀6 小时前
【深度学习|可视化】如何以图形化的方式展示神经网络的结构、训练过程、模型的中间状态或模型决策的结果??
人工智能·python·深度学习·神经网络·机器学习·计算机视觉·cnn
CyreneSimon7 小时前
使用 LoRA 进行模型微调的步骤
python·transformer
ymchuangke7 小时前
数据清洗-缺失值处理-缺失值可视化图(竖线)
python·算法·数学建模