众所周知,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的打包工具来进行打包编译安装等。

参考资料

  1. https://zhuanlan.zhihu.com/p/106411447
  2. https://docs.python.org/3/c-api/arg.html#c.PyArg_Parse
  3. https://pythonhowto.readthedocs.io/zh_CN/latest/pythonc.html