在之前的一篇博客中,我通过一个项目streamcpy展示了如何用C语言写一个Python模块,受到了不少人关注(表示感谢你🙏),于是就打算出一个系列的文章专门介绍Python的C语言API。通过这个系列的文章,读者不仅可以对Python的C语言API有一定的了解,还可以一步步的实现一个模仿官方datetime
的Python模块datetimecpy
(repo戳这里)。如果能给大家带来启发就最好啦。
Python的C语言API有什么用
在这一切的开始之前,我想先介绍一下Python的C语言API的“用武之地”——
Python的C语言API可以让Python加速
(敲黑板)这个是重点。由于GIL的存在,Python的运行速度一直让人诟病。目前市面上有很多Python加速方案,比如cython、codon等工具。但是这些工具都是将Python语言往C方面优化,如果直接用官方的C语言API可以达到更好的效果。
Python的C语言API可以做本地化适配
和Java一样,Python也是一门跨平台的高级语言。但是跨平台的语言需要对不同的操作系统、CPU硬件做不同的适配,这时候就需要C语言来作为底层的支撑,比如PyQT这种针对不同平台封装不同图形界面的库。也有的通过C语言专门提供一些本平台特定的功能,比如win32可以给用户提供Windows操作系统的能力。还有的会将加速和平台适配结合起来,比如tensorflow这种需要底层加速以及硬件适配的机器学习框架。实际上大多数用C语言开发的Python库都是出于这个目的。
Python的C语言可以更好的学习CPython
不同于C/C++等编译型语言,Python是解释型语言。当我们写好Python代码后,解释器会将它翻译成字节码,而解释字节码本质上就是在调用C语言API。有的人认为写Python就是在写C语言,其实这个观点某种程度上是说得通的。因此,熟悉C语言API对阅读CPython源码也很有帮助。
CPython是指用C语言实现的Python,也就是我们一般所说的Python。除此以外还有Jython(Java实现),Pypy(Python自举实现)和IronPython(C#实现)等。
现在正片开始——
环境配置
学习一门技能避免不了配置环境,但是Python的C开发环境很简单。除此以外最好还有一个Debug工具。
如果你是Windows用户
下载C语言编译器,这里建议使用最新版本的Visual Studio(我用的是Visual Studio 2022)。如果觉得太大至少需要下载Visual Studio Build Tools。说句题外话,作为宇宙第一IDE的Visual Studio难道不能占用你电脑几十G的内存吗你🐶?
然后安装两个“工作负荷”,分别是“Python开发”和“使用C++的桌面开发”
好了,就那么多。现在开始尝试创建一个项目。选择“Python扩展模块”。
然后和其他C/C++项目一样,创建项目。
完了以后会生成一个模板,这个模板可以运行的。如果弹出下面这个控制台就说明OK了。
如果你是Mac/Linux用户
除了安装python以外还需要安装python的开发包。
apt-get install python-dev
有的发行版是python-devel这个包。
然后再是C语言编译器,这里推荐GCC,装上就行。
apt-get install gcc
苹果用户可以通过brew安装
搞一个Debug工具
由于是跨语言debug,所以有点麻烦。Windows环境就用Visual Studio自带的,但是会提示缺失符号文件,这个在Python的Installer里面有。
选择“调试”-“选项...”-“符号”可以把刚刚添加的.pdb
文件添加进去。
如果是Mac/Linux用户就麻烦一点,需要使用GCC的配套工具GDB,具体参考这个博客。
要点讲解
Python的C扩展开发思想
首先需要搞懂Python的C语言API是什么?它有什么作用?
我是这样理解的:Python的C语言API是CPython解释器的后端接口,它为用户提供了一套绕过词法分析、语法分析等编译步骤的,直接操作虚拟机的API。它是Python语言的 “平替” 。
传统的编译器(解释器)分成两个部分——前端和后端。前端的范围指词法分析、语法分析和中间表示(IR)生成,而后端指中间表示优化到机器码生成(或执行)。所以C语言API可以跳过中间表示之前的一系列步骤。
既然定位是“平替”,那么C语言API可以代替Python代码的执行,也就是说所有的Python程序都可以用C语言改写,但是反过来不行。这里就体现了C语言API的价值了。
C语言API规范
其次,再总览一遍C语言API的接口。文档点这里。可以看到这里面将API分三个层次归纳理解,分别是非常高层次(The very high level layer)、抽象对象层次(Abstract objects layer)和具体抽象层次(Concrete objects layer)。除此以外还需要注意以下几点:
- API的稳定性:有的API会标注Limit API,这表明这个API是限制的,也就是在整个3.X版本都适用。
- "_ "的使用:底杠表示私有的,这个规则在Python的语境里似乎通用,比如带底杠前缀的API表示只有当前小版本适用,带底杠的文件代表不编译(或者不对外暴露),带底杠的变量代表内部共享。、
- API命名规范:这里又是重点。所有标准API都会以Py开头,后面跟操作实体,然后接底杠再是camel-case的方法名,比如
PyList_Append
,这个方法表示标准方法,而且作用于Python的list类型变量,并且用于追加某个元素。实际上这个方法等同于Python的append
方法。 - API的宏:除了方法以外,这套API还提供了不少有用的宏。同样也是Py开头,并且后接大写字母,比如
Py_INCREF
和Py_DECREF
。这两个宏分别表示增加引用计数和减少引用计数(或销毁对象)。这两个宏是CPython内存管理的核心,第二章会介绍用法,第七章会讲内存管理具体介绍。 - API的返回值引用属性:目前分为两种,分别是借用(Borrowed Reference)和新增(New Reference)。由于涉及到引用计数,所以不可避免涉及到所有权(类似C++和Rust中所有权的概念)。简单一句话,具有借用属性的变量不需要我们对其进行引用计数操作(比如
Py_INCREF
),否则需要纳入引用计数管理。具体第七章会介绍。
操作实践
我猜很多读者是直接跳到这里开始阅读的你😁,当然也没问题,毕竟这里才是核心。
本系列文章是要实现一个datetime模块,叫datetimecpy。这个模块我会尽量保持官方对datetimecpy的兼容。此外,可以通过切换分支选择不同章节的代码,比如本章的对应代码在分支Ch-1上。
前面说了C语言开发的Python模块就是对Python开发的“平替”,所以我们不妨先用Python打一个“草稿”,也就是最终的调用方法。
>>>import datetimecpy
>>>datetimecpy.now()
1683207793
有一个Python包叫datetimecpy(准确来说叫扩展),里面就一个方法叫now
,它不接受任何参数,然后返回当前的时间戳。当然这个时间戳用C语言相关API获取。
就这么简单的一个扩展,现在要用C语言来实现——
首先找到Visual Studio自动为我们生成的datetimecpy.c
文件,这个是我们模块的入口文件,但是现在我们需要把原先的内容全删了。然后写下下面代码:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
这两行是Python的C语言程序的头文件第一行定义了一个宏,它和解析参数有关,暂且不管。第二行引入了Python.h
头文件,这里面囊括了所有Python的C语言API,并且包括原生的C语言库,比如stdlib.h
等。
接下来实现now
方法。注意这是一个模块的方法,不属于任何类,所以这样写——
#include <time.h>
PyObject*
datetimecpy_now(PyObject* self, PyObject* Py_UNUSED(args)) {
time_t now = time(NULL);
if (now < 0) {
Py_RETURN_NONE;
} else {
return PyLong_FromLongLong((long long)now);
}
}
引入头文件不必多说,然后重点来了——我们需要定义一个PyObject* (*)(PyObject*, PyObject*)
类型的函数。这是一个标标准准的PyCFunction
函数,第一个参数类型为PyObject* ,它在不同语境下有不同的涵义,而这里代表当前模块,即datetimecpy。第二个参数类型也是PyObject* ,它代表这个now
方法的所有参数,参数是通过列表传入的(平替了* args)。由于这类没用到外部传进来的参数,所以打上Py_UNUSED
宏。最后这个函数返回了一个PyObject* ,这个返回值代表Python的一个类型,结合后面的语句,它可以是None
或者int
。
函数体里面需要抓住两个重点,一个是Py_RETURN_NONE
宏,另一个是PyLong_FromLongLong
方法。Py_RETURN_NONE
宏可以扩展成如下两个操作:
Py_INCREF(Py_None)
return Py_None
Py_None
就是C语言版本的None
,它是为数不多的单例,也是一个object
。所以先需要增加引用计数,然后再返回。有关引用计数的内容在第二章讲述。这类是当time
调用失败的情况下返回的。
根据前文讲述的C语言API规范,PyLong_FromLongLong
方法是一个公有的,作用于Long的一个方法。直白点说,它是讲C语言的long long
类型的变量转换成Python的int
变量。这里就是将时间戳转换成Python的int
对象返回。
现在我们继续——
static PyMethodDef datetimecpy_functions[] = {
{ "now", (PyCFunction)datetimecpy_now, METH_NOARGS, PyDoc_STR("now\n-\n\nQuery the current timestamp from the os.") },
{ NULL, NULL, 0, NULL }
};
既然我们实现了now
函数,就要注册这个函数。这是一个PyMethodDef
类型的数组,数组元素第一项是Python版本的方法名(当然是now了),第二个是对应的函数(就是我们刚刚实现的),然后是属性,比如METH_NOARGS
(说明不接受任何参数),最后是文字描述。
由于只有一个方法,所以后面就用{ NULL, NULL, 0, NULL }
实例作为结束的哨兵(Sentinel)。
然后编写模块的文字描述,并定义这个模块——
PyDoc_STRVAR(datetimecpy_doc, "The datetimecpy module");
static PyModuleDef datetimecpy_def = {
PyModuleDef_HEAD_INIT,
"datetimecpy",
datetimecpy_doc,
-1, /* m_size */
datetimecpy_functions, /* m_methods */
NULL, /* m_slots */
NULL, /* m_traverse */
NULL, /* m_clear */
NULL, /* m_free */
};
模块通过PyModuleDef
定义,并且包含公有的模块头PyModuleDef_HEAD_INIT
。然后紧接着是模块名称,文字描述,大小(这里是-1,代表不能作为共享属性信息的模块,具体会在第五章说明),包含方法(即刚刚的PyMethodDef
),其他属性暂且不涉及。
最后,再将其嵌入到Python解释器——
PyMODINIT_FUNC PyInit_datetimecpy() {
PyObject* m = PyModule_Create(&datetimecpy_def);
if (!m)
{
return NULL;
}
PyModule_AddStringConstant(m, "__author__", "littlebutt");
PyModule_AddStringConstant(m, "__version__", "1.0.0");
return m;
}
我们可以简单粗暴的把它理解成main函数,在Python执行import语句时会调用。在这个函数里,先通过PyModule_Create
将对应的datetimecpy_def
实例化生成模块,模块也是一个object,然后再为其添加作者和版本信息,并将其返回。
现在,我们运行这个程序,并调用now
方法。如果出现下面这样就说明成功了。
到目前为止,如果不出错的话就恭喜你编写了第一个Python的C扩展!
但是且慢,我们还没有真正的将这个扩展打包投入到生产中去。打包的方法也简单,还是setuptools
。
新建一个setup.py
文件
from setuptools import setup, Extension
module1 = Extension('datetimecpy',
sources = ['datetimecpy.c'])
setup (name='datetimecpy',
version='1.0',
author='littlebutt',
author_email='luogan1996@icloud.com',
license='MIT',
description = "The datetime API",
url='https://github.com/littlebutt/datetimecpy',
ext_modules=[module1])
这样即可,然后我们在控制台运行
python setup.py build
就可以利用C语言编译器生成对应的包
再运行
python setup.py install
就可以安装这个包,然后在其他项目中使用啦。
小结
本章介绍了一些我对Python的C语言API的理解和看法,然后介绍了API相关特性,最后实现了一个简单的C扩展模块。在下面一章会重点讲解PyObject这个对象了。