前期的网页抽取算法使用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
7PyObject* 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
23PyObject* 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}
};
2.导出对象
除了上面提到的使用复杂的PyObject操作语法封装一个Python对象返回之外还有其他途径,如直接导出C的Struct到Python。这里不详谈,需要的可以查相关资料。
3.初始化模块
模块初始化调用Py_InitModule,传入模块名和模块的方法描述表即可。如果初始化失败会返回error可以做相应处理。
1
2
3
4PyMODINIT_FUNC initecho()
{
Py_InitModule("echo", EchoMethods);
}
三、编译与使用
1.如何编译、分发、使用
上面这些代码当然会用到python-devel库。编译的时候使用GCC直接编译成一般的so,就可以直接在python里面调用了。Python会自己选择如何加载这个so。
1
2g++ -cecho.c -I/usr/include/python2.7/include/python2.7 -fPIC
g++ -sharedecho.o -oecho.so
上面已经提到了,实际上把自己编译好的so放在PYTHONPATH路径中的任意一个下面都可以直接调用了。
2.更便捷的方式
上面的编译方式可以自己写一个Makefile处理起来更灵活,实际上Python有一个更方便的处理方式。使用distutils包,编译安装一步到位,这也是easy_install等工具使用的方式。
上面这个简单使用distutils处理起来像这样:
1
2
3
4
5
6
7
8from distutils.coreimport setup, Extension
echomodule= Extension("echo",
sources= ["echo.c"])
setup(name= "echo",
version= "1.0",
description= "test",
author= "dudu"
ext_modules= [echomodule])
Extension对象定义一个扩展的源文件、需要用到的第三方库、头文件、特殊的编译选项等等,而setup则定义安装的规则及扩展的一些属性。
使用的时候执行下面两个命令就可以了。
1
2python setup.py build
sudo python setup.pyinstall
这部分可以参考http://docs.python.org/2/distutils/apiref.html
文章是写完了。特别推荐需要开发许多接口的人去看看开头提到的swig/sip等等,这些项目只需要编写简单的规则,就可以为c/c++中的方法生成wrapper。这里只所以有采用c api是因为需求简单,需要暴露给python的总共也没几个函数。
作者:麦田守望
就职于盛大创新院,主要从事搜索引擎研发等工作。熟悉C/C++,Python,Node.JS