众所周知,python最大的不足就是慢,为了解决这个问题,可以通过写原生C模块来提高python的执行效率。像numpy等都是用C写的。
由于python(Cpython)的底层是通过C实现的,所以python有一套完整的C API,可以方便地写C模块。
现在以mandelbrot分形图为例,用C写一个python模块,来加速计算过程。mandelbrot分形图的计算是一种常见的评估编程语言性能的方法。关于mandelbrot分形图,可以参考这个。这个例子的目的是使用C重写mandelbrot的核心计算过程,以大大提升python的执行效率。
要使用C扩展,首先要有C的那一套编译环境,能够正常编译C程序即可,一个简单的python的C扩展模块实际上是一个可以与python交互的动态链接库。
先新建一个C语言文件cal.c
,写下如下代码:
#include <Python.h>
这个表示引入Python的头文件,里面定义了一些关键的与python交互有关的函数。
其次写计算mandelbrot的函数,这个函数的输入是一个点在复平面上的坐标(实部与虚部),逃逸判定半径与最大迭代次数,输出是一个整数,表示这个点的迭代次数。
static short mpdog(double a, double b, int T, float M)
{
double x=0;
double y=0;
double c=0;
double d=0;
int i = 0;
for (; i < T; i++)
{
c=x*x-y*y;
d=2*x*y;
c=c+a;
d=d+b;
if (c*c+d*d>M)
{
return i;
break;
} else
x=c;
y=d;
}
return i;
}
返回值这里用的C语言里的short
类型,对应的是numpy里的int16
类型。主要的计算过程就这些,剩下的就是与python有关的内容了。
然后需要写一个入口函数,用来承接python的调用,并把结果反馈给python。
static PyObject * mcssdog(PyObject *self, PyObject * args)
{
double a = 0;
double b = 0;
int c=0;
float d=0;
if (!PyArg_ParseTuple(args, "ddif", &a, &b, &c, &d))
return NULL;
short r = mpdog(a, b,c,d);
return Py_BuildValue("h", r);
}
这个函数的参数是PyObject类的指针,用来传递从python那边调用时的参数,需要通过PyArg_ParseTuple
这个函数来解析,并把相应的参数值放置到对应的内存地址中。PyArg_ParseTuple(args, "ddif", &a, &b, &c, &d)
中第一个参数是PyObject类的指针,表示调用时传递的参数,原则上应该是这个函数收到的第二个形参。第二个参数是一个字符串,表示参数的格式,后面的都是存放相关参数的地址。"ddif"
表示参数的格式依次是是double
两个参数,int
一个参数,float
一个参数。常见的类型与参数对照表可以参考python的文档。PyArg_ParseTuple
如果解析成功,则返回1,否则返回0,这个的用法和PyArg_ParseTuple
的用法差不多,第一个是返回值的类型,后一个则是返回值。
解析成功后,这个函数就可以开始计算了,然后把结果Py_BuildValue
返回给python。
接下来的内容就是告诉python这个模块的相关信息了,首先要列出模块的方法。
static PyMethodDef dogMethods[] =
{
{"mcssdog", mcssdog, METH_VARARGS, " mandelbrot set"},
{NULL, NULL, 0, NULL}
};
这看上去比较复杂,实际上是一个结构体数组的初始化……结构体的四个成员按顺序是方法名(在python中调用时的名字),函数指针(在C里面的函数名),方法的参数传递方式,方法的描述。这个结构体数组应当以一个成员为NULL的结构体结尾。
剩下的模块相关的信息了
static struct PyModuleDef caadogm =
{
PyModuleDef_HEAD_INIT,
"mcssdog",
"dog",
-1,
accuMethods
};
PyMODINIT_FUNC PyInit_mcssdog(void) {
return PyModule_Create(&caadogm);
}
首先又是一个结构体的初始化,结构体的五个成员,第一个是固定写法,没有特别的意义。第二个是模块名在python里调用时使用(要注意,这个名应当和后面初始化函数PyInit_
下划线后面的部分以及最后编译出来的动态链接库.so
的文件名一致)。第三个是模块的描述。第四个是刚才定义的的结构体数组名。然后就是初始化函数了,将刚才定义的模块有关的结构体的地址传给PyModule_Create
即可。
完成之后就可以进行编译了,一般可以用gcc编译
gcc -I/usr/include/python3.8 -o mcssdog.so mc.c -fPIC -shared
要加上python的头文件的位置,这个可以通过运行 python -c "import sysconfig; print( sysconfig.get_path('include') )"
来获取。
正确编译之后,就可以使用python调用这个函数了。
from mcssdog import mcssdog
print(mcssdog(-0.0015,0.025,500,4))
除了可以直接编译成动态链接库以外,也可以用python的打包工具来进行打包编译安装等。
参考资料