前期的网页抽取算法使用C++开发,为了提升代码复用,减少维护成本,项目中决定封装成Python扩展方便Python使用。
Python与C/C++互操作有很多方案:Python C API, swig, sip, ctypes, cpython, cffi, boost.python等。这里选择了最原始的Python C API方式。
大多数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
Python对象管理采用引用技术模型,内部有一些复杂的循环引用等处理措施。主要有 Py_INCREF() / Py_DECREF()两个宏负责处理。具体文档可以看这里http://docs.python.org/2/c-api/intro.html#reference-counts
例如上一点申请的对象obj如果需要释放怎么办?不可以直接free/delete,直接Py_DECREF(obj),然后obj = NULL即可,否则会报错。
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
有了上面的知识我们开始进行实际的开发。
写好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 errorn" ); return NULL; } int count = 0; do { printf ( "%sn" , input); count++; } while (count < 100); return Py_BuildValue( "i" , 0); } static PyMethodDef EchoMethods[] = { { "echo" , (PyCFunction)echo, METH_VARARGS}, {NULL, NULL} }; |
除了上面提到的使用复杂的PyObject操作语法封装一个Python对象返回之外还有其他途径,如直接导出C的Struct到Python。这里不详谈,需要的可以查相关资料。
模块初始化调用Py_InitModule,传入模块名和模块的方法描述表即可。如果初始化失败会返回error可以做相应处理。
1 2 3 4 | PyMODINIT_FUNC initecho() { Py_InitModule( "echo" , EchoMethods); } |
上面这些代码当然会用到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路径中的任意一个下面都可以直接调用了。
上面的编译方式可以自己写一个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的总共也没几个函数。
作者:麦田守望
就职于盛大创新院,主要从事搜索引擎研发等工作。熟悉C/C++,Python,Node.JS
联系客服