最经常使用的调用C函数的方式,分别是c extension,Cython和ctypes。python
python标准库包含了不少使用C开发的扩展模块,好比对性能要求很高的json库。开发者一样可使用C开发扩展,这是最原始也是最底层的扩展python的方式。linux
python的扩展模块由如下几部分组成:json
// pulls in the Python API #include <Python.h> // C function always has two arguments, conventionally named self and args // The args argument will be a pointer to a Python tuple object containing the arguments. // Each item of the tuple corresponds to an argument in the call’s argument list. static PyObject * demo_add(PyObject *self, PyObject *args) { const int a, b; // convert PyObject to C values if (!PyArg_ParseTuple(args, "ii", &a, &b)) return NULL; return Py_BuildValue("i", a+b); } // module's method table static PyMethodDef DemoMethods[] = { {"add", demo_add, METH_VARARGS, "Add two integers"}, {NULL, NULL, 0, NULL} }; // module’s initialization function PyMODINIT_FUNC initdemo(void) { (void)Py_InitModule("demo", DemoMethods); }
编译扩展模块一般使用distutils或setuptools,它会自动调用gcc完成编译和连接。多线程
from distutils.core import setup, Extension module1 = Extension('demo', sources = ['demomodule.c'] ) setup (name = 'a demo extension module', version = '1.0', description = 'This is a demo package', ext_modules = [module1])
执行app
python setup.py build_ext --inplace
会在当前目录生成一个demo.so
。一个python扩展模块其实就是一个共享库(.so),它能够直接在python解释器中import。函数
--inplace
表示将生成的扩展放到源码所在的目录,即当前目录,这样就能够直接import而不须要安装到site-packages目录。oop
测试性能
>>> from demo import add >>> add(1,1) 2 >>> add(1,2) 3 >>> add(1) Traceback (most recent call last): ... TypeError: function takes exactly 2 arguments (1 given) >>> add(1,'2') Traceback (most recent call last): ... TypeError: an integer is required
Cython听起来像是一种语言,c与python的结合,这么说其实没有错。python是一种动态类型的解释型语言,执行效率低,Cython在python的基础上增长了可选的静态类型申明的语法,代码在使用前先被转换成优化过的C代码,而后编译成python扩展库,大大提高了执行效率。所以从语言的角度来说,Cython是python的超集,即扩展了的python。测试
注意不要和CPython混淆,CPython是用c实现的python解释器,由官方提供,咱们平时使用的python就是CPython。另外,pypy是python本身实现的python解释器。Cython是cpython标准库的一部分,不须要额外安装。优化
用官网的一句话介绍Cython的做用:
extending the CPython interpreter with fast binary modules, and interfacing Python code with external C libraries.
简单的说,Cython的两个主要做用是:
如今使用Cython从新实现上面的例子——编写C函数的包装器。
最终的目录结构以下
. ├── add_wrapper.c ├── add_wrapper.pyx ├── add_wrapper.so ├── build │ └── temp.linux-x86_64-2.7 │ └── add_wrapper.o ├── libadd.a ├── libadd.c ├── libadd.h ├── libadd.o └── setup.py
libadd.h
int add(int a, int b);
通常都是经过python调用动态连接库,须要将生成的库文件(.so)安装到标准路径下(好比/usr/lib)下,连接和运行的时候才能找到该文件,为了方便这里以静态连接库为例。
首先将c文件编译成静态连接库:
gcc -c libadd.c ar rcs libadd.a libadd.o
第一步会在当前目录下生成libadd.o
,第二步建立静态连接库libadd.a
。
使用Cython包装C函数
使用Cython调用c函数很简单,只须要在Cython中声明函数的签名,而后编译的时候正确地连接外部的动态或静态库。
下面就是一个add函数的python包装器: add_wrapper.pyx
cdef extern from "libadd.h": cpdef int add(int a, int b)
第一行表示引入头文件libadd.h
。第二行声明该头文件中的add
函数,直接从libadd.h拷贝过来便可,此时只有在Cython模块内部能调用该C函数,还须要在前面加cpdef
声明,表示暴露出接口给python调用。
Cython是须要编译成二进制模块才能使用的,编译过程包含两步:
怎么编译呢?最经常使用的方式是编写一个setup.py
文件:
from distutils.core import setup, Extension from Cython.Build import Cythonize ext_modules=[ Extension("add_wrapper", sources=["add_wrapper.pyx"], extra_objects=['libadd.a'] ) ] setup( name = 'wrapper for libadd', ext_modules = Cythonize(ext_modules), )
extra_objects
表示须要连接的静态库文件,也能够替换成libraries=["add"],library_dirs=["."]
,链接器会自动搜索libadd.so
和libadd.a
,动态连接库优先。
执行
python setup.py build_ext --inplace
在当前目录下会生成add_wrapper.c
和add_wrapper.so
,add_wrapper.c
是第一步编译生成的中间文件,内容比较长。add_wrapper.so
是最终的python二进制模块,将它放到PYTHONPATH的某个路径下,就能够直接import。
若是须要从新build,你可能须要加上--force
选项,不然可能不会生效。
>>> from add_wrapper import add >>> add(1,1) 2 >>> add(2,3) 5 >>> add(-1,1) 0 >>> add(1,False) 1 >>> add(1) Traceback (most recent call last): ... TypeError: wrap() takes exactly 2 positional arguments (1 given) >>> >>> add(1,'1') Traceback (most recent call last): ... TypeError: an integer is required
因而可知,Cython会自动检查参数类型并完成python对象到C类型的转换。
ctypes的主要做用就是在python中调用C动态连接库(shared library)中的函数。
libadd.c
int add(int a, int b) { return a + b; }
gcc -shared -o libadd.so libadd.c
使用CDLL动态加载共享库,一个共享库对应一个cdll对象。调用cdll的LoadLibrary()方法或直接调用CDLL的构造函数建立一个CDLL对象。
>>> from ctypes import * >>> mylib = CDLL('/home/yanxurui/test/keepcoding/python/extension/ctypes/libadd.so')
第二行的CDLL
等价于cdll.LoadLibrary
。
若是共享库不在标准路径/usr/lib
下则须要使用完整的路径。 ctypes提供了find_library
用来找到共享库的位置,可是find_library会查找/usr/local/lib
,所以搜索成功不表明也能加载成功。有人也反映了这个bug:
经过访问dll对象的属性来调用相应的函数,就像调用python的函数对象同样:
>>> mylib.add <_FuncPtr object at 0x7ff6864b7bb0> >>> add = mylib.add >>> add(1,2) 3 >>> add() 1 >>> add(1) -2044290911 >>> add(1,'a') -2042137139
ctypes并不会校验参数的数量和类型,经过设置函数的argtypes
的属性能够指定函数参数的类型:
>>> add.argtypes = [c_int, c_int] >>> add(1, 2) 3 >>> add(1) Traceback (most recent call last): ... TypeError: this function takes at least 2 arguments (1 given) >>> add(1, '2') Traceback (most recent call last): ... ctypes.ArgumentError: argument 2: <type 'exceptions.TypeError'>: wrong type
另外,原生的python类型中只容许传入None, 整数, 字符串做为函数的参数。若是须要传递其余的类型,则须要使用ctypes定义的类型,好比c_double表示double。
从上面看出,c扩展虽然复杂,但更接地气,性能必然也是最好的,而Cython和ctypes开发效率奇高。
调用C库的一个主要目的是优化性能,所以咱们更关心三种方式对性能的影响。 下面经过一个简单的benchmark来比较,即便10000000次加法操做也很快,很难看出调用C函数对性能带来的提高,但这无所谓,由于咱们的主要目的是对比不一样调用方式在调用共享库时的性能开销。
测试的代码以下,因为模块名以及import的方式不一样,因此每次测试须要稍微修改一下注释的地方。
from time import time # c ext # from demo import add # Cython # from add_wrapper import add # ctypes # mylib = CDLL('/home/yanxurui/test/keepcoding/python/extension/ctypes/libadd.so') # add = mylib.add # add.argtypes = [c_int, c_int] # python # def add(a,b): # return a+b s=time() for i in range(10000000): r = add(i, i) print(time()-s)
10000000 loops, best of 3:
method | cost(s) |
---|---|
c ext | 2.522 |
Cython | 1.723 |
ctypes | 8.896 |
python | 1.879 |
测试的结果让人惊讶:
对于这个测试的结果,我没法盲目的相信,还须要进一步的探究。
若是已经有一个现成的库,我会选择使用Cython或ctypes做为包装器,若是还须要考虑性能的话,固然就是Cython了。