最經常使用的調用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了。