最新消息:

Python的C扩展-应用与陷阱

c admin 2676浏览 0评论

1.首先

Python作为一种流行的动态脚本语言,既有面向对象的数据抽象能力,也具备脚本语言快速开发易学易用的一般优点.不过应用中也会发现,它确实有点慢,而且因为性能上的欠缺被挂上"最慢的脚本语言"的恶名.

但其实不完全如此,python易于扩展的特点促使其通过技术集成来弥补其先天不足,与c等外部语言强强联合,使其可以胜任绝多数的应用需求.就c扩展而言,python通过一套较为完整的API,实现了这两类语言代码间的紧密沟通,即python内嵌与python扩展。

最近一次应用中我们需要用python处理大数据,比如分析上千万PACK包,这种情况下把最主要的处理通过c来实现,而外部调度和用户交互由python完成,可为一种两全之策,即下面所讨论的python之c扩展。

2. Python扩展的用武之地-库测试

如果拿到一个c编写的库需要测试,通常情况下最直接的方法是写c代码来一个个调用,然后对于函数接口来预谋好的各类异常参数组合来考验接口实现是否正确并强壮,但很明显地你会发现,为了检查各个不同case输入情况下库接口的反应,可能需要很多次的重新编译与链接,如果做过这种事,你还会赞同一点:这种做法烦人之处并不只是在反复build测试代码上浪费时间.可能你会想到先准备一些有格式的case,然后由程序自己解析批量把各种情况下调用全做了,然后一起来检查(这个检查也有可能自动化),但再想想库接口的调用间往往并非完全独立,这样你还得组合函数调用次序,再查起结果来是不是更头痛。

事实上,这种纠结的原因在于我们对程序动态运行中测试交互的需求不易满足,如果能一边执行,一边看,麻烦应该会少很多。

(1)动态库的测试

如果拿到的是c动态库就很方便了,因为从python代码直接就调用库接口.例如./example.so中有以下导出接口原型:

Int parse_3des_certificate(const char*, int);

可以如下来交互地测试此接口:

>>> import ctypes
>>> lib=ctypes.cdll (“./example.so”)
>>> func=lib.parse_3des_certificate
>>> print “ret=%d” % func(“test”, 100)

如果传递的c的复合数据类型,如类或结构体(结构体视为类的特化即可统一两者),也可以通过ctype来互通,如对于c库接口:

char* mpeg4_getcoder_info(decord_t*, short);

其中decord_t定义为:

typedef struct _decord_t {
unsigned long decorder_id;
__int64    guid_algo;
char trait_code[255];
}decord_t;

在python中调用此接口时只是需要加一些ctypes的转换即可:

>>> import ctypes
>>> lib=ctypes.cdll(“./example.so”)
>>> func=lib.mpeg4_getcoder_info

# 指定函数的返回类型

>>> func.restype=ctypes.c_char_p

# 建立python类与c类转换进而数据交换: 分别指定各个各个数据成员的名字和ctypes类型

>>> class py_decord_t(ctypes.Structure) :
>>>    _fields_=[(“decorder_id”, ctypes.c_ulong), (“guid_algo”, ctypes.c_longlong), (“trait_code”, ctypes.c_char_p)]

# 建立python对象并初始化

>>> py_decorder=py_decorder_t()
>>> py_decorder.decorder_id=292
>>> py_decorder.guid_algo=10021
>>> py_decorder.trait_code=’DDSN1001’

# 用python对象调用c库接口,数据传递给对应的c对象

>>>print func(ctypes.byref(py_decorder), 1)

诸如此法,便可交互地调用接口,从而辅助测试.这里用到了一个关键的python模块ctypes,它用于实现两种语言数据类型上的转换传递.另外,要将 c代码编译成动态库以便如上述在python中调用其接口,可以使用一个通用的c程序扩展脚本语言的编译器SWIG, 它不仅仅用于扩展python,还可用于生成在perl,Tcl中调用的c动态库。

详见 http://www.swig.org/Doc1.3/Python.html.

ctypes的细节则在 http://docs.python.org/library/ctypes.html 可以看到。

可以看出,上面是在用交互模式运行python解释器,当然如果事先已经准备了case和预期结果而且也设计好了数据解析的格式,那用python来实现一个自动化测试就是比较轻松的事情了,免去在这个>>>后面枯燥地敲代码之烦.当然,两种方式也是各有优劣的,看具体需要来应用即可。

(2)静态库的测试

如果要测的是个c静态库,似乎python就帮不上啥忙了,因为python并不支持静态库接口调用的(其它语言也做不到动态调用,否则就不叫静态库了).不过其实有一个很笨但确实简单的方法,自己转换出一个动态库来:写一个小的c程序lib_dummy.c,对所有关心的接口依次写个wrapper 即可.比如现在提交的example.a中有以下接口:

int get_sitekw_data(const char* file, const char* site, int sitelen, char* buf, int buflen);

相应地只要在lib_dummy.c实现一个

Void get_sitekw_data_so(const char* file, const char* site, int sitelen, char* buf, int buflen)
{
return   get_sitekw_data(file, site, sitelen, buf, buflen);
}

其它接口如法炮制,然后用它build一个动态库就可以了。

● 陷阱之一:

不过,有一点不得不提,因为生成这个动态库时显然要联编提交的静态库,要求这个静态库在编译时使用-fpic选项,否则生成动态库时会出错. (编译时简单地多用一个选项和为了测试又特地去提供一个对应的动态库,这两者我想前者应该更容易被人接受,而且现在通常生成静态库时一般都会加上这个选项,因为它使静态库应用更加便利了) 有了动态库,后面的测试过程就和上述一样了。

3 python模块级扩展

又是引入ctypes来转换数据类型,还要专门搬来一个什么swig,上面的python扩展方法用起来末免显得有点别扭,况且这两个东东还是要单独安装的(如果装的是python2.6以上版本要幸运一点儿,因为ctypes已经被纳入其标准库中了). 我们想要的扩展是无缝的,就是像使用如sys, string,pickle这些标准python库一样地使用我们扩展的模块,因为这样就不用再考虑什么两种语言间的数据类型转换了。

当然,其实这种python编码时的舒畅是用c编码来实现这个模块时的稍稍头痛一点换来的,事实了语言间的数据类型沟通问题是转移到c代码这一层面来了,不过相比在python中每使用一次都要被数据转换所扰,这种一劳常逸的方式之好处就不言而喻了。

这种用c实现python模块扩展的功能是由一套完整的python c API提供的(声明于Python.h中),概括起来体现为如下三步曲:

1) 要扩展的模块接口,用c实现,即一个个稍有点另类的c函数;下面主要就是说这个当中的问题.如

static PyObject* func(PyObject* self, PyObject* args)

2) 要导出的模块接口列表,如

static PyMethodDef
all_exported_interfaces [] = {
{“func”, func, METH_VARARGS, “a c func as an example”},
{NULL, NULL}

3) 模块初始化函数,即给python解释器看的扩展模块的entry point.

PyMODINIT_FUNC
initlibname()
{
PyObject* gork = Py_InitModule(“libname”, all_exported_interfaces);
}

这三部曲模式在http://www.python.org/doc/ext/intro.html完整而细致的说明.

● 陷阱之二:

不过在很多地方看到的是初始化函数返回类型是声明为void, 这样在python中使用扩展的模块接口时就会出问题,解释器说:初始化函数没有定义,即没有找到entry point initlibname. 可能在别的编译环境下没有这个问题,但在我们现在的开发机编译器下void取代为PyMODINIT_FUNC才能解决这个问题.这是一个条件定义的预编译宏,python环境会自动选择它是否最终就是 void. 当然, 最后生成的动态库名字与init函数名字中”init”后面部分必须一致, Py_InitModule第一个入参也得用这个名字.这是API文档中明示的

这三部曲中后二步基本是固化的,重点在于第一步.如官方文档中所述,实现一个python扩展模块常用的api包含

PyArg_ParseTuple, Py_BuildValue, PyList_xxx, PyString_xxx, PyTuple_xxx, PyDict_xxx, ….

等等.使用方法在文档中容易参照实施,照葫芦画瓢即可.

举例来说,要扩展一个如下python接口(标识的python类型仅作示意, python本身是基于非严格数据类型的语言):

a_python_string func (a python string, a python list of string, a python_dict, a python_int)

实现过程因为对性能要求较高而采用c来写,而外部控制则因交互需要由python实现.从需求可见,要解决的关键问题即从传入的python string/list/dict/int等数据解析成c的数据类型,得到了C数据后处理过程就是C代码的事情,暂时与python无关. String, int, float等等之类是直接对应到c的char*, int,float的,直接画瓢就行了(参考http://www.python.org/doc/2.5.2/api/arg- parsing.htm), 按照此页面上的说明画出瓢如下:

#define MAX_FIELD_NUM    100
#define MAX_ITEM_SIZE 255

//parse out the entry parameters
int sz_pack_type = 0;
int comb_mode = 1;
char *pack_type[MAX_ITEM_SIZE];
ostringstream ostr_errmsg;
PyObject* pList = PyList_New(MAX_FIELD_NUM);
PyObject* pDict = PyDict_New(MAX_FIELD_NUM);
PyListObject type_obj_list;
PyDictObject type_obj_dict;
if (!pList || !pDict) goto escape;
ret = PyArg_ParseTuple(args, “s#:error|O!O!i:error”, &pack_type, &sz_pack_type, & type_obj_list, pList, & type_obj_dict, pDict, &comb_mode);
if (-1 == ret) || !PyList_Check(pList) || !PyList_Check(pDict)) {
ostr_errmsg << “error in parsing entry parameters” << endl << ends;
goto escape;
}
pack_type[sz_pack_type] = ‘′;
// 用解析出的list, dict等数据进行处理
…..

● 陷阱之三:

如此得到的扩展模块在python代码中引用后, 发现list,dict数据根本没有传递成功-后面对pList, pDict访问其元素时代码core掉. 经过多方求解, 才知这段程序中有三大误区:

(1)用来存储解析结果数据的c栈内存空间由API层来管理, 无须在扩展的c代码中分配,因此pack_type, pList, pDict均初始化为NULL即可.

(2)解析函数参数二的format string中, :指示参数列表的结束,后面的格式符对应的python入参将不会被解析!

(3)最关键所在: 当需要直接将传入的python对象解析为一个API层次的python对象时,解析函数的入参实际应为二重指针! 另外, 如果用O!格式解析, 除了要传给解析函数一个授受此对象的二重指针外, 还需要一个叫做typeobject的参数. 因为没有相应示例, 便误解为如上所用的一个PyListObject或PyDictObject对象的指针. 实际上这里需要使用API中定义好的”类型对象”, 如PyList_Type或PyDict_Type, 不要误解为这是一个类型名,看看其定义才知, 它是一个实例

PyAPI_DATA(PyTypeObject) PyList_Type

另外,还需要注意的一点: 如果设计的扩展模块接口本身需要用可变入参列表(即类似于多个重载版本, 这样接口的使用可以更加灵活通过), 可以如上在format string中使用”|”来标识, 其意为后面的参数都可选. 此时一定要对optional的参数预先进行默认值初始化,否则在python代码里调用后可能有意想不到的乱子.

修改后, 一个可用的瓢如下:

//parse out the entry parameters
char *pack_type=NULL;
int comb_mode = 1;
ostringstream ostr_errmsg;
PyObject* pList = NULL;
PyObject* pDict = NULL;
Py_ssize_t sz_list=0, sz_dict=0;
memset(pack_type, 0, sizeof pack_type);
ret = PyArg_ParseTuple(args, “s|O!O!i:func”, &pack_type, &PyList_Type, &pList, &PyDict_Type, &pDict, &comb_mode);
if (-1 == ret){
ostr_errmsg << “error in parsing entry parameters” << endl << ends;
goto escape;
}

后面对解析到的list或dict的操作使用相应的API即可,如对list的元素的访问:

if (pHeadFeatureList)
sz_head_rules = PyList_Size(pHeadFeatureList);
if (sz_head_rules > 0) {
for (Py_ssize_t idx = 0; idx < sz_head_rules; idx++) {
PyObject* pcur_head_rule = PyList_GetItem(pHeadFeatureList, idx);
if (!PyString_Check(pcur_head_rule))     {
ostr_errmsg << “Parsing head rules failed: not a type of python string” << endl << ends;
goto escape;
}
ostr_head_rules << PyString_AsString(pcur_head_rule) << “n”;
}
ostr_head_rules << endl;
}
对dict, tuple也有类似接口,详见http://docs.python.org/c-api/

最后, 可能需要将处理的数据以python数据类型返回,这时使用Py_BuildValue函数即可, 使用方法只要理解了解析参数时的format string就比较容易.如

return Py_BuildValue(“s”, ostr_errmsg.str().c_str());

如果是过程式的模块接口,也还是要为了满足接口原型中PyObject*这一返回类型的要求, 模板式地返回一个PyNone;

完成以上步骤后,就可以直接用gcc/g++来生成动态库, 再次提醒自己: 如果联编了其它静态库,这个库必须是用-fpic选项编译出来的( 好在像ullib这些公共库都是如此生成,其它一般公共库只要作者有此扩展应用的考虑都不会忘记这一点, 我用了一个自己的c库, 便重新用-fpic再编译一次后才正常用于动态库的编译.

在python中使用这样的扩展模块就与使用标准库没有什么不同了, 如

#!/usr/bin/env /home/spider/python
Import libname
Print libname.func(“type”, [“line1”, “line2”,…”linen”], [1:”abc”, 2:”cdb”, …., x:”xxx”], 2)

Print libname.func(“typex”)

4 小结

以上python C API应用的二个分支之一,另一类是在C/c++代码中调用python模块, 这样的应用需求通常来源于为了节省开发时间, 将一些繁琐的如涉及复杂数据格式解析与封装等处理放在python模块中处理, 而涉及计算量大,或网络请求并发响应之类的处理放在主语言C代码中完成, 就可以实现另一种维度的强强联合了.

关于python内嵌扩展, 完整的官方文档可以参考 http://docs.python.org/c-api/intro.html#embedding-python.

转载请注明:爱开源 » Python的C扩展-应用与陷阱

您必须 登录 才能发表评论!