python下的c/c++扩展方式详解

python作为一种胶水语言,最重要的是非常容易的和shellc/c++之间进行相互扩展,而本文主要是对pythonc/c++扩展进行详细讲解,并作为笔记便于以后的使用。

为什么要进行c/c++扩展

python之所以使用广泛,主要是因为使用简单,第三方扩展库强大,对于某个问题,能迅速的验证自己的思路或某个原型。但是,python的效率是比较低的,因此很多涉及到效率的时候需要使用c/c++来实现,为python提供接口即可。这样混合编程后,能达到快速+效率的双重目的。

python的c/c++扩展

python下的c/c+扩展有很多方式,主要有cffi/ctypescpythoncython这几种方式,各个方式都有优势。下面对这几个方式进行详细的说明。

cffi来进行c扩展

cffi是连接cpython的桥梁,是在pypy中分离出来的一个库,这个和python自带的ctypes模块很相似,但是cffi更加通用。下面我们定义了一个pi.hpi.c文件:

1
2
3
4
5
// file: pi.h
#ifndef C_EXTEND_PYTHON_PI_H
#define C_EXTEND_PYTHON_PI_H

float pi_approx(float n);#endif *//C_EXTEND_PYTHON_PI_H*
1
2
3
4
// file: pi.c
float pi_approx(float n){
return 3.1415926 * n * n;
}

使用cffi模块来对这两个文件进行编译,会生成一个c source文件和.so.pyd文件,可以在python下直接引用,进行使用,编译文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from cffi import FFI

ffi_builder = FFI()
ffi_builder.cdef("""
float pi_approx(float n);
""")
# This describes the extension module "_pi_cffi" to produce.
ffi_builder.set_source("_pi_cffi",
"""
#include "pi.c" // the C header of the library
""",
libraries=[]) # library name, for the linker

if *__name__ == "__main__":
ffi_builder.compile(verbose=True )

直接运行该编译文件则可创建python的c扩展库:.so.pyd文件;另外也会生成_pi_cffi.c的源码。将.so.pyd文件放如对应的目录即可实现引用。这种方式比较方便,能迅速构建c扩展。

cpython的方式

这种方式,是利用Python的C-API接口来构建python模块。Python的每个版本官方都有相应的C-API文档,每个版本API可能存在一些不同。特别是Python2和Python3(本文是基于Python3的版本)之间。下面我们就通过代码来进行说明。主要实现一个列表中所有元素的和。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/*
* #include "/opt/anaconda3/include/python3.6m/Python.h"
* Python.h has all the required function definitions to manipulate the Python objects
*/
#include "D:\Anaconda3\include\Python.h"

//This is the function that is called from your python code
static PyObject* addList_add(PyObject* self, PyObject* args){

PyObject * listObj; // 申请一个python对象来存list

// args参数解析, 并将值存入到listObj变量
if (! PyArg_ParseTuple( args, "O", &listObj ))
return NULL;

// 获取list的长度
long length = PyList_Size(listObj);

// 循环获取list中的值, 并求和
long i, sum =0;
for (i = 0; i < length; i++) {
//get an element out of the list - the element is also a python objects
PyObject* temp = PyList_GetItem(listObj, i);
//we know that object represents an integer - so convert it into C long
long elem = PyLong_AsLong(temp);
sum += elem;
}

//value returned back to python code - another python object
//build value here converts the C long to a python integer
return Py_BuildValue("l", sum);

}


// 这里是为上面的函数定义一个docstring
static char addList_docs[] =
"add( ): add all elements of the list\n";

/*
* This table contains the relavent info mapping -<function-name in python module>, <
* actual-function>,<type-of-args the function expects>, <docstring associated with the function>
*/

static PyMethodDef addList_funcs[] = {
{"add", (PyCFunction)addList_add, METH_VARARGS, addList_docs},
{NULL, NULL, 0, NULL}

};

/*defined PyModuleDef struct*/
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"addList", // module name
NULL,
-1,
addList_funcs, // functions
NULL,
NULL,
NULL,
NULL
};


/*
* addList is the module name, and this is the initialization block of the module.
* <desired module name>, <the-info-table>, <module's-docstring>
*/
static PyObject *SpamError = NULL;


// 创建模块
PyMODINIT_FUNC PyInit_addList(void){
PyObject *m;
m = PyModule_Create(&moduledef);
if (m == NULL)
return NULL;
SpamError = PyErr_NewException("addList.error", NULL, NULL);
Py_INCREF(SpamError);
PyModule_AddObject(m, "error", SpamError);
return m;
}

完成上面功能后,编写一个setup.py即可:

1
2
3
from distutils.core import setup, Extension

setup(name='addList', version='1.0', ext_modules=[Extension('addList', ['addList.c'])])

运行python setup.py build即可进行编译。编译后悔生成相应的.so.pyd文件文件:模块名称为addList。python可以直接进行引用该文件即可。
这种方法比较灵活,但是需要对python的C-API相对有了解。官方是推荐这种方法来进行扩展的,比其他方法都要原生,对c++也支持的非常好。

CYTHON的方式进行扩展

cython可以看成一个python的超集,不仅结合了c的语法,同时能使用c的东西。它的运行过程主要是将cython实现的代码转成c代码,并进行编译形成Python的扩展库。cython不仅仅进行了Python与C之间的转换,还对部分操作进行了优化。在cython可以使用c的任何东西, 同时支持Python的函数和类的方式编程。目前很多第三方库都有使用它,比如scikit-learnscipy等等很多。具体的一些用法可以参考它的官方文档。下面我通过cython从不同的角度来展示它的用法。

使用cython连接c文件和python

假设现在我写一个c的一些方法,包括了头文件和源文件:

1
2
3
4
5
6
// file: csample.h
#include <stdio.h>
#include <math.h>

int add(int x, int y);
int sub(int x, int y);
1
2
3
4
5
6
7
8
// file: csample.c
int add(int x, int y){
return x + y;
}

int sub(int x, int y){
return x - y;
}

现在我要用cython来使用上面定义的c方法,首先创建一个cython的头文件csample.pxd文件,文件中引用csample.h的方法,,并定义一个Point结构体:

1
2
3
4
5
6
7
8
// file csample.pxd
cdef extern from "csample.h":
int add(int x, int y)
int sub(int x, int y)

ctypedef struct Point:
double x
double y

再新建一个pyx的文件,通过cython来定义python的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// file: sample.pyx
cimport csample

from cpython.pycapsule cimport *
from libc.stdlib cimport malloc, free


def add(x, y):
return csample.add(x, y)


def sub(x, y):
return csample.sub(x, y)


cdef delPoint(object obj):
pt = <csample.Point *> PyCapsule_GetPointer(obj, "Point")
free(<void *> pt)


def Point(x, y):
cdef csample.Point *p
p = <csample.Point *> malloc(sizeof(csample.Point))
if p == NULL:
raise MemoryError("No memoery to make a point!")

p.x = x
p.y = y
return PyCapsule_New(<void *> p, "Point", <PyCapsule_Destructor>delPoint)

这样整个过程就实现了,基本和上面cpython的过程类似,只是采用的方式不同。下面编写一个setup.py的文件即可进行编译python setup.py build。生成对一个的扩展包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# file: setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [
Extension('sample', # 扩展包名称
['sample.pyx', 'E:\\WorkSpace\\2019\CythonExample\\c_mix_cython\\csample.c'], # 用到的源文件
libraries=[], # 依赖的库
library_dirs=['.'])]
setup(
name='Sample extension module',
cmdclass={'build_ext': build_ext},
ext_modules=ext_modules
)

在cython中使用头文件(pxd)

头文件的使用具有一定的好处,方便大量代码的维护等等。在cython中也可以使用头文件,在上面的例子中已经应用到了。下面再进行说明。首先创建一个头文件, 定义相应的方法,结构等:

1
2
3
4
5
6
7
# sample.pxd
cdef int function1(int a, int b=*)
cdef double add_one(double x)

cdef class A:
cdef public int a, b
cpdef void foo(self, double x)

再根据头文件实现对应的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# file: sample.pyx
cdef int function1(int a, int b=10):
cdef int x = a - b
return x + a * b

cdef double add_one(double x):
return x + 1


cdef class A:
def __init__(self, int b=0):
self.a = 13
self.b = b

cpdef void foo(self, double x):
print(x + add_one(10.25))

下面编写setup.py进行编译即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [
Extension('sample',
['sample.pyx'],
libraries=[],
library_dirs=['.'])]
setup(
name='Sample extension module',
cmdclass={'build_ext': build_ext},
ext_modules=ext_modules
)

在cython中使用指针(pointer

在cython中也是可以使用指针的,可以将python对象转化为指针在cython中使用,下面是具体的示例,代码中给了注释说明,这里就不详细介绍了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from cpython cimport array
from cython.parallel import prange
cimport numpy as np
import cython
import numpy as np

ctypedef unsigned long long ullong

cdef inline ullong add_list(unsigned long *array, unsigned long size):
"""注意数据类型,在c中, int, long, long long等不同的数据类型, 边界不同, 因此必须要满足所有的数据在最大边界内, 否则结果是不正确的"""
cdef ullong i = 0
cdef ullong sum_v = 0
for i in prange(size, nogil=True):
sum_v += array[i]
return sum_v


def addList(list_array, long s):
"""将一个python对象list传递给一个函数, 该函数的接收参数是一个指针(int *)类型, 通过指针来操作python的list对象.
reference: Cython: can't convert Python object to 'double *':
https://stackoverflow.com/questions/17014379/cython-cant-convert-python-object-to-double
"""
cdef array.array p_array =array.array('L', list_array) # transform c array
return add_list(p_array.data.as_ulongs, s)

def addList_numpy(list_array, long s):
"""通过numpy的ndarray将list对象转换为c的指针类型
reference: tutorials Numpy Pointer To C with Cython
https://github.com/cython/cython/wiki/tutorials-NumpyPointerToC
"""
cdef np.ndarray[long, ndim=1, mode='c'] p_array = np.array(list_array) # transform c memory type
return add_list(<unsigned long *> &p_array[0], s) # &p_array[0] or <int*> p_array.data

在cython中使用结构体(struct)

cython可以使用c的任何东西,因此结构体也是支持的,这里用一个小的例子来说明在cython中的使用。代码如下所示,如果在cython中返回结构体,对应到python中就是一个字典的对象。

1
2
3
4
5
6
7
8
9
10
11
ctypedef struct Node:
double x
double y

cdef Node node

def setNode(double x, double y):
cdef Node *p_node = &node
p_node.x = x
p_node.y = y
return p_node[0] # 返回了字典

总结

上面主要对cffi, cpythoncython的三种方式进行了详解。这三种方式也是主流的方式,其中比较常用的应该是cython的方式,特别是和numpy的结合。其次是cffi的方式,但是还是推荐cpython的方式,这种方式更加原生,和python结合的更好,但是,难度也是最高的。在工作应用中可以根据自己的需要选择不同的方式。