前期的網頁抽取算法使用C++開發,爲了提升代碼複用,減少維護成本,項目中決定封裝成Python擴展方便Python使用。
Python與C/C++互操作有很多方案:Python C API, swig, sip, ctypes, cpython, cffi, boost.python等。這裏選擇了最原始的Python C API方式。
一、開發前準備
1.Python對象
大多數Python對象在Python解析器中都爲PyObject,在C代碼中只能聲明PyObject*類型的python對象,然後使用該對象對應的初始化函數初始化。如PyTuple_New,PyList_New,PyDict_New,Py_BuildValue等。
例如構建一個{‘a’:{‘b’:['123','34']}}對象
1
2
3
4
5
6
7
|
PyObject*
obj = PyDict_New(); PyObject*
b = PyDict_New(); PyObject*
c = PyList_New(2); PyList_SetItem(c,
0, Py_BuildValue( "s" , "123" )); PyList_SetItem(c,
1, Py_BuildValue( "s" , "34" )); PyDict_SetItem(a, "b" ,
c); PyDict_SetItem(obj, "a" ,
a); |
Python對象問題這裏有一些文檔:
http://docs.python.org/2/c-api/intro.html#objects-types-and-reference-counts
http://docs.python.org/2/c-api/dict.html
http://docs.python.org/2/c-api/list.html
2.Python內存管理
Python對象管理採用引用技術模型,內部有一些複雜的循環引用等處理措施。主要有 Py_INCREF() / Py_DECREF()兩個宏負責處理。具體文檔可以看這裏http://docs.python.org/2/c-api/intro.html#reference-counts
例如上一點申請的對象obj如果需要釋放怎麼辦?不可以直接free/delete,直接Py_DECREF(obj),然後obj = NULL即可,否則會報錯。
3.線程安全
Python由於歷史比較悠久,作者在開發的時候可能並沒有考慮到多線程這個東西,因爲Python的內存管理並不是線程安全的。在後來後來版本中爲了處理這個線程安全問題引入了GIL即global interpreter lock。這是一個粗粒度的鎖,執行Python ByteCode之前都會取得這個鎖。以至於Python的多線程比較雞肋,GIL也就成了性能瓶頸。這個問題很多地方都有討論,我之前有一篇文章專門對這個問題進行了說明,感興趣的同學請去這裏http://in.sdo.com/?p=1623。
有人會問爲什麼不設計更細粒度的鎖?實際上有人已經進行了嘗試,但是爲了不增加實現的複雜性也就一直沒有加到CPython中。其他版本的python如IronPython等對這個問題已經做了改善。
實際開發時有兩種情況需要關心:
1).釋放鎖
這種情景只要在進行IO或CPU繁重的計算時,暫時釋放GIL使得其他線程的代碼可以執行。
2).取得鎖
主要出現在C回調Python代碼
參考文檔:
http://docs.python.org/2/c-api/init.html#thread-state-and-the-global-interpreter-lock
二、開發擴展
有了上面的知識我們開始進行實際的開發。
1.導出函數
寫好C API函數之後我們需要導出,寫一個函數描述表即可,如下面的EchoMethods,一定要以NULL結尾。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
PyObject*
echo(PyObject* self, PyObject* args) { char *
input = NULL; if (!PyArg_ParseTuple(args, "s" ,
&input)) { printf ( "parse
arg error\n" ); return
NULL; } int
count = 0; do { printf ( "%s\n" ,
input); count++; } while (count
< 100); return
Py_BuildValue( "i" ,
0); } static
PyMethodDef EchoMethods[] = { { "echo" ,
(PyCFunction)echo, METH_VARARGS}, {NULL,
NULL} }; |
2.導出對象
除了上面提到的使用複雜的PyObject操作語法封裝一個Python對象返回之外還有其他途徑,如直接導出C的Struct到Python。這裏不詳談,需要的可以查相關資料。
3.初始化模塊
模塊初始化調用Py_InitModule,傳入模塊名和模塊的方法描述表即可。如果初始化失敗會返回error可以做相應處理。
1
2
3
4
|
PyMODINIT_FUNC
initecho() { Py_InitModule( "echo" ,
EchoMethods); } |
三、編譯與使用
1.如何編譯、分發、使用
上面這些代碼當然會用到python-devel庫。編譯的時候使用GCC直接編譯成一般的so,就可以直接在python裏面調用了。Python會自己選擇如何加載這個so。
1
2
|
g++
-c echo .c
-I /usr/include/python2 .7 /include/python2 .7
-fPIC g++
-shared echo .o
-o echo .so |
上面已經提到了,實際上把自己編譯好的so放在PYTHONPATH路徑中的任意一個下面都可以直接調用了。
2.更便捷的方式
上面的編譯方式可以自己寫一個Makefile處理起來更靈活,實際上Python有一個更方便的處理方式。使用distutils包,編譯安裝一步到位,這也是easy_install等工具使用的方式。
上面這個簡單使用distutils處理起來像這樣:
1
2
3
4
5
6
7
8
|
from
distutils.core import
setup, Extension echomodule =
Extension( 'echo' , sources =
[ 'echo.c' ]) setup(name =
'echo' , version =
'1.0' , description =
'test' , author =
"dudu" ext_modules =
[echomodule]) |
Extension對象定義一個擴展的源文件、需要用到的第三方庫、頭文件、特殊的編譯選項等等,而setup則定義安裝的規則及擴展的一些屬性。
使用的時候執行下面兩個命令就可以了。
1
2
|
python
setup.py build sudo
python setup.py install |
這部分可以參考http://docs.python.org/2/distutils/apiref.html
文章是寫完了。特別推薦需要開發許多接口的人去看看開頭提到的swig/sip等等,這些項目只需要編寫簡單的規則,就可以爲c/c++中的方法生成wrapper。我只所以有采用c api是因爲需求簡單,需要暴露給python的總共也沒幾個函數。