构建 Python C 扩展模块
有好几种扩展 Python 的功能的方法。其中一种就是用 C 或 C++ 编写 Python 模块。通过这个过程可以提高性能,更好地访问 C 库函数和系统调用。在本教程中,我将带大家了解如何使用 Python API 来编写 Python C 扩展模块。这里说的都是 Cpython。
通过本教程你将学到
在 Python 内部执行 C 的函数
将参数通过 Python 传到 C 并依次解析它们
从 C 代码中引发异常,并在 C 中创建自定义的 Python 异常
在 C 中定义全局常量,并在 Python 中访问它们
打包和发布Python C扩展模块
拓展你的 Python 程序
Python 的一个不太为人所知但却非常强大的特性是,它能够调用 C 或 C++ 等编译语言定义的函数和库,扩展 Python 内置特性以外的功能。
拓展 Python 功能有很多的语言可以选择,那么为什么选择 C 语言呢?
以下是使用 C 构建 Python 扩展模块的一些原因:
实现新的内置对象类型 其中一个就是 Python 底层就是 C 语言实现的,还有就是我们可以从Python 本身实例化和扩展该类。
调用 C 库函数和系统调用 许多编程语言为最常用的系统调用提供接口,不过,可能还有其他较少使用的系统调用只能通过 C 访问。Python 中的 os模块就是一个例子。
要用 C 语言编写 Python 模块,你需要了解 Python API,它定义了允许 Python 解释器调用你的 C 代码的各种函数、宏和变量。所有这些工具以及更多的工具都打包在Python.h头文件中。
用 C 语言编写 Python 接口
在本教程中,你将为一个 C 库函数编写一个包装器,然后在 Python 中调用它。自己实现包装器可以更好地了解何时以及如何使用 C 来扩展 Python 模块。
理解 C 语言中的 fputs()
fputs()就是我们将要包装的 C 库函数。下面是 fputs() 函数的声明。
int fputs(const char *str, FILE *stream)
这个函数有两个参数:
1.str :这是一个数组,包含了要写入的以空字符终止的字符序列。
2.stream * :这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流,该函数返回一个非负值,如果发生错误则返回 EOF。
下面的实例演示了 fputs() 函数的用法。
#include
#include
#include
int main() {
FILE *fp = fopen("write.txt", "w");
fputs("I Love NightTeam!", fp);
fclose(fp);
return 1;
}
简单总结上面的代码:
1.打开一个当前目录下叫 write.txt 的文件
2.然后在这个文件中写入 "I Love NightTeam!"
在下一节中,我们将为该函数提供更丰富的功能。
包装 fputs() 函数
首先我们看最终完善之后的代码
#include
static PyObject *method_fputs(PyObject *self, PyObject *args) {
//str 是要写入文件流的字符串。
//filename 是要写入的文件的名称。
char *str, *filename = NULL;
int bytes_copied = -1;
/* Parse arguments */
if(!PyArg_ParseTuple(args, "ss", &str, &filename)) {
return NULL;
}
FILE *fp = fopen(filename, "w");
bytes_copied = fputs(str, fp);
fclose(fp);
return PyLong_FromLong(bytes_copied);
}
接下来,我们来一点点分析上面的代码。
我们这里面的代码是按照Python API来写的,第一行
#include
我们通过它导入 Python.h 这个头文件,在 C 语言里是没这个头文件的,不过不用担心,它在后期Python 运行的时候会找到对应的文件。
这段代码中引用了 Python.h 中定义的三个对象结构。
1.PyObject
2.PyArg_ParseTuple()
3.PyLong_FromLong()
这些都是用于定义 Python 语言的数据类型,开头都是 Py,现在我们一一来看。
PyObject
PyObject 是用于为 Python 定义对象的类型。所有的 Python 对象都是在 PyObject 基础上进行拓展的,比如 Python 中的 int,在 C 语言中实际上是一个 PyLongObject 函数。PyObject 告诉 Python 解释器将指向对象的指针视为对象。例如,将上述函数的返回类型设置为 PyObject,这就定义了 Python 解释器所需的公共字段。
PyArg_ParseTuple
PyArg_ParseTuple() 将从 Python 程序接收的参数解析为局部变量,返回一个整型。
相关代码片段
if(!PyArg_ParseTuple(args, "ss", &str, &filename)) {
return NULL;
}
它的语法是这样的
int PyArg_ParseTuple(PyObject* tuple,char* format,...)
1.args:参数arg必须是一个元组对象,包含一个从Python传递给C函数的参数列表
2."ss":是一个格式参数它必须是格式字符串,初次之外还有很多个参数,最后面我会给出参考地址。
3.&str 和 &filename:可变参数,指向局部变量的指针,解析后的值将赋给这些局部变量。
这里我们的例子是 PyArg_ParseTuple() 如果执行失败结果为 false 。如函数将返回 NULL,不再继续。
fputs()
如前所述,fputs()有两个参数,其中一个是 FILE * 对象。由于在 C 语言中无法使用 Python API 解析 Python textIOwrapper 对象,因此必须使用一种变通方法
FILE *fp = fopen(filename, "w");
bytes_copied = fputs(str, fp);
fclose(fp);
然后,将 fputs() 的返回值存储在 bytes_copied 中。 该整数变量将返回到 Python 解释器中的fputs()调用
PyLong_FromLong(bytes_copied)
PyLong_FromLong() 返回一个 PyLongObject,它在 Python 中表示一个整数对象。通过它将返回一个 PyObject 对象给 Python。
编写 Init 函数
我们已经编写了构成 Python C 扩展模块核心功能的代码。 但是,仍然需要一些额外的功能来启动和运行模块。需要编写模块及其包含的方法的定义,如下所示:
static PyMethodDef FputsMethods[] = {
{"fputs", method_fputs, METH_VARARGS, "Python interface for fputs C library function"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef fputsmodule = {
PyModuleDef_HEAD_INIT,
"fputs",
"Python interface for the fputs C library function",
-1,
FputsMethods
};
这些函数包括有关模块的元信息,Python 解释器将使用这些元信息。让我们看看上面的每个结构是如何工作的。
PyMethodDef
这是一个函数列表,因为我们一般会定义多个函数,使用 {NULL, NULL, 0, NULL} 表示最后一个函数。
先看第一部分代码
static PyMethodDef FputsMethods[] = {
{"fputs", method_fputs, METH_VARARGS, "Python interface for fputs C library function"},
{NULL, NULL, 0, NULL}
};
函数列表的单个元素,由4个参数组成。第一个参数是用户要调用的函数名称,第二个是要调用的C函数名称,第三个是模块的标示,告诉解释器函数将接受两个 PyObject 类型的参数,self 模块对象和arg 函数的实际参数的元组。第四个就是函数的 docstring ,我们可以通过 help(fputs) 获取。
PyModuleDef
正如 PyMethodDef 保留有关 Python C 扩展模块中方法的信息一样,PyModuleDef 结构也保留有关模块本身的信息。但是它不是结构的数组,而是用于模块定义的单个结构。
static struct PyModuleDef fputsmodule = {
PyModuleDef_HEAD_INIT,
"fputs",
"Python interface for the fputs C library function",
-1,
FputsMethods
};
第一个参数固定写就可以了,第二个参数是 Python C 扩展模块的名称。第三个参数表示模块docstring 的值。第四个参数模块空间,一般子解释器使用的,-1 表示不使用,第五个参数就是上面定义的函数列表。
PyMODINIT_FUNC
既然已经定义了 Python C 扩展模块和方法结构,现在就该使用它们了。当 Python 程序第一次导入模块时,它将调用 PyInit_fputs()
PyMODINIT_FUNC PyInit_fputs(void) {
return PyModule_Create(&fputsmodule);
}
PyMODINIT_FUNC 在声明为函数返回类型时隐式地做了三件事:
1.它将函数的返回类型隐式设置为 PyObject *。
2.它声明任何特殊的链接。
3.它将函数声明为 extern C。如果你在使用 C++,它会告诉 C++ 编译器以 C 的方式运行。
PyInit_ 作为固定开头,然后加模块的名字 fputs。
PyModule_Create() 将返回一个类型为 PyObject * 的新模块对象。参数传入的是上面定义的fputsmodule。
注意:在 Python3 中,你的 init 函数必须返回一个 PyObject * 类型。但是,如果使用的是Python2,那么 PyMODINIT_FUNC 将函数返回类型声明为 void。
回顾整个过程
现在我们已经编写了 Python C 扩展模块的必要部分,让我们回过头来看看它们是如何组合在一起的。下图显示了模块的组件以及它们如何与 Python 解释器交互
当你通过 Python 导入 fputs 模块的使用,首先会进入 PyInit_fputs 这个入口函数,在将引用返回给 Python 解释器之前,该函数随后调用 PyModule_Create(),它将初始化 PyModuleDef 和 PyMethodDef 函数,其中包含关于模块的元信息。准备好它们是有意义的,因为你将在 init 函数中使用它们。完成之后,对模块对象的引用最终返回给 Python 解释器。下图显示了模块的内部流程
PyModule_Create() 返回的模块对象有一个对模块结构 PyModuleDef 的引用,该结构又有一个对方法 PyMethodDef 的引用。当你调用在 Python C 扩展模块中定义的方法时,Python 解释器使用模块对象及其携带的所有引用来执行特定的方法。同样,你可以访问模块的各种其他方法和属性,例如模块 docstring 或方法 docstring。 这些定义在它们各自的结构内部。
现在你已经了解了从 Python 解释器调用 fputs() 时会发生什么,解释器使用模块对象以及模块和方法引用来调用方法。最后,让我们看看解释器如何处理 Python C 扩展模块运行的:
调用 fputs() 方法后,程序将执行以下步骤:
1.使用 PyArg_ParseTuple() 解析从 Python 解释器传递的参数
2.将这些参数传递给 fputs(),这是构成模块核心的 C 库函数。
3.使用 PyLong_FromLong 从 fput() 返回值
最后是完整代码
#include
static PyObject *method_fputs(PyObject *self, PyObject *args) {
//str是要写入ss文件流的字符串。
//filename是要写入的文件的名称。
char *str, *filename = NULL;
int bytes_copied = -1;
/* Parse arguments */
if(!PyArg_ParseTuple(args, "ss", &str, &filename)) {
return NULL;
}
FILE *fp = fopen(filename, "w");
bytes_copied = fputs(str, fp);
fclose(fp);
return PyLong_FromLong(bytes_copied);
}
static PyMethodDef FputsMethods[] = {
{"fputs", method_fputs, METH_VARARGS, "Python interface for fputs C library function"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef fputsmodule = {
PyModuleDef_HEAD_INIT,
"fputs",
"Python interface for the fputs C library function",
-1,
FputsMethods
};
PyMODINIT_FUNC PyInit_fputs(void) {
return PyModule_Create(&fputsmodule);
}
打包 Python C 扩展模块
在导入新模块之前,首先需要构建它。可以通过使用 Python 的 distutils 模块实现这一点。
下面先上代码,文件名setup.py
from distutils.core import setup, Extension
def main():
setup(name="fputs",
version="1.0.0",
description="Python interface for the fputs C library function",
author="cxa",
author_email="1598828268@qq.com",
ext_modules=[Extension("fputs", ["fputsmodule.c"])])
if __name__ == "__main__":
main()
代码很简单,我主要是解释下 setup 里面的参数函数含义,
name 就是打包文件名称,version 版本号,一般都是 1.0.0 开始的。description 就是模块描述,ext_modules 是一个数组类型,Extension("fputs", ["fputsmodule.c"]),Extension里面第一个参数是模块,第二个参数注意它是一个列表类型。它表示的是我们编写好的 C 文件的路径。
构建模块
现在你已经有了 setup.py 文件,可以使用它来构建 Python C 扩展模块了。
构建非常简单一句话就可以了
python3 setup.py install
该命令将编译并安装当前目录下的Python C扩展模块。如果失败了就根据具体错误信息,百度搜下就可以解决了。
运行你的模块
现在一切都就绪了,是时候看看你的模块是如何工作的了!
>>> import fputs
>>> fputs.__doc__
'Python interface for the fputs C library function'
>>> fputs.__name__
'fputs'
>>> # Write to an empty file named `write.txt`
>>> fputs.fputs("NightTeam!", "write.txt")
13
>>> with open("write.txt", "r") as f:
>>> print(f.read())
'NightTeam!'
引发异常
Python 异常与 C++ 异常非常不同。如果希望从 C 扩展模块中引发 Python 异常,那么可以使用Python API 来实现。Python API 提供的一些用于异常引发的函数如下
函数名
描述
PyErr_SetString(PyObject *type, const char *message)
带有两个参数:一个PyObject *类型的参数,指定异常的类型,以及一个向用户显示的自定义消息
PyErr_Format(PyObject *type,const char *format)
带有两个参数:一个PyObject *类型的参数,指定异常的类型,以及一个向用户显示的格式化自定义消息
PyErr_SetObject(PyObject *type, PyObject *value)
接受两个参数,都是PyObject *类型:第一个参数指定异常的类型,第二个参数设置一个任意的Python对象作为异常值
你可以使用其中任何一个来引发异常。但是,使用哪一个以及何时使用完全取决具体的需求。Python API拥有所有预先定义为PyObject类型的标准异常。
从C代码中引发异常
虽然在C语言中不能引发异常,但Python API允许你从Python C扩展模块中引发异常。我们通过向代码中添加PyErr_SetString()来测试这个功能。
static PyObject *method_fputs(PyObject *self, PyObject *args) {
char *str, *filename = NULL;
int bytes_copied = -1;
/* Parse arguments */
if(!PyArg_ParseTuple(args, "ss", &str, &fd)) {
return NULL;
}
if (strlen(str) < 10) {
PyErr_SetString(PyExc_ValueError, "String length must be greater than 10");
return NULL;
}
fp = fopen(filename, "w");
bytes_copied = fputs(str, fp);
fclose(fp);
return PyLong_FromLong(bytes_copied);
}
在这里,在解析参数之后和调用 fputs() 之前,检查输入字符串的长度。如果用户传递的字符串小于10 个字符,则程序将使用自定义消息引发 ValueError 错误。一旦异常发生,程序执行就会停止。
注意上面的 fputs() 方法在引发异常后返回了一个 NULL。这是因为只要你使用 PyErr_*()引发异常。不需要调用函数来随后再次设置该条目。因此,调用函数返回一个指示失败的值,通常为NULL或-1。(这也应该解释为什么当使用 PyArg_ParseTuple()解析 method_fputs()中的参数时,为什么需要返回 NULL。)
增加自定义异常
你还可以在 Python C 扩展模块中引发自定义异常。但是,使用方法和上面有所不同。在前面的PyMODINIT_FUNC 中,你只需返回由 PyModule_Create 返回的实例即可。但是如果让使用模块的用户能够访问自定义异常,就需要在返回之前将自定义异常添加到模块实例。
static PyObject *StringTooShortError = NULL;
PyMODINIT_FUNC PyInit_fputs(void) {
/* 分配模块值 */
PyObject *module = PyModule_Create(&fputsmodule);
/* 初始化新的异常对象 */
StringTooShortError = PyErr_NewException("fputs.StringTooShortError", NULL, NULL);
/* 将异常对象添加到模块中 */
PyModule_AddObject(module, "StringTooShortError", StringTooShortError);
return module;
}
与前面一样,首先创建一个模块对象。然后使用 PyErr_NewException 创建一个新的异常对象。第一个参数采用 module.classname 的形式作为要创建的异常类的名称,选择描述性内容,以使用户更容易解释实际出了什么问题。接下来,使用 PyModule_AddObject 将其添加到模块对象中。第一个参数是上面创建的模块对象,第二个参数是异常对象的名称,第三个参数
就是异常对象本身。最后返回模块对象。
既然已经定义了新的异常方法,那么我们就可以将核心代码改为下面这样:
static PyObject *method_fputs(PyObject *self, PyObject *args) {
char *str, *filename = NULL;
int bytes_copied = -1;
/* Parse arguments */
if(!PyArg_ParseTuple(args, "ss", &str, &fd)) {
return NULL;
}
if (strlen(str) < 10) {
/* Passing custom exception */
PyErr_SetString(StringTooShortError, "String length must be greater than 10");
return NULL;
}
fp = fopen(filename, "w");
bytes_copied = fputs(str, fp);
fclose(fp);
return PyLong_FromLong(bytes_copied);
}
之后打包,构建生成新的模块。通过下面的代码进行测试
>>> import fputs
>>> # Custom exception
>>> fputs.fputs("NT!", fp.fileno())
Traceback (most recent call last):
File "", line 1, in
fputs.StringTooShortError: String length must be greater than 10
如果字符串长度小于 10,这个时候我们定义异常就会抛出了。
定义常量
在某些情况下,需要在 Python C 扩展模块中使用或定义常量。这与您在前一节中定义自定义异常的方式非常相似。可以使用 PyModule_AddIntConstant() 定义一个新常量并将其添加到模块实例中。
PyMODINIT_FUNC PyInit_fputs(void) {
/* Assign module value */
PyObject *module = PyModule_Create(&fputsmodule);
/* Add int constant by name */
PyModule_AddIntConstant(module, "FPUTS_FLAG", 64);
/* Define int macro */
#define FPUTS_MACRO 256
/* Add macro to module */
PyModule_AddIntMacro(module, FPUTS_MACRO);
return module;
}
其中
PyModule_AddIntConstant(module, "FPUTS_FLAG", 64);
里面包含三个参数,分别是模块的名字,常量的名称和常量的值。
你还可以使用 PyModule_AddIntMacro() 对宏执行相同的操作。
/* 定义宏 */
#define FPUTS_MACRO 256
/* 添加宏到模块*/
PyModule_AddIntMacro(module, FPUTS_MACRO);
重新打包构建并运行观察结果
>>> import fputs
>>> # Constants
>>> fputs.FPUTS_FLAG
64
>>> fputs.FPUTS_MACRO
256
我们发现,可以从Python解释器中访问这些常量。
考虑替代方案
在本教程中,你已经为C库函数构建了一个接口,以了解如何编写 Python C 扩展模块。但是,有时你需要做的只是调用一些系统调用或一些C库函数,并且希望避免编写两种不同语言的开销。在这些情况下,你可以使用 Python 库,如 ctypes 或 cffi。
关于 ctypes 是的使用可以看我公众号之前写的文章。
总结
在本教程中,你学习了如何使用 Python API 以 C 编程语言编写 Python 接口。 为 C 库函数fputs() 编写了一个 Python 包装器。在构建之前,我们还向模块添加了自定义异常和常量。
Python API 为用 C 编程语言编写复杂的 Python 接口提供了大量特性。同时,像 cffi 或ctypes 这样的库可以降低编写 Python C 扩展模块所涉及的开销。所以应该按照自己的需求选择合理的拓展方式。