rss· 投稿· 设为首页· 加入收藏· 繁體版
当前位置: 火魔网 » 程序开发 » Python

python基础之c扩展类

   在zope中使用了很多用c写的扩展类,使用c写python的扩展类通常出于两个原因:1、提升速度和节省内存。2、封装一些现成的功能库。zodb 中的cPersistent模块就是用c来写的,为了更好地理解zodb的设计和实现理念,我们现在先来看看使用c写python扩展类的基本知识. 1、扩展类的文件和函数命名规则(习惯编程模式)
   如果最后生成的module是 mtest,也就是在python中以“import mtest”的形式调用,那么在组织源文件的结构如下:
   mtestmodule.h
   mtestmodule.c
        |---> PyMODINIT_FUNC initmtest(void)-->"metestmodule"不是必须的,但是initmtest是必须的,而且名字也是固定的,这个函数是 在“import mtest”的时候又解析器自动调用,PyMODINIT_FUNC是 void function(void) 的兼容c/c++的宏
   供python调用c模块的函数的标准处理流程是:
python基础之c扩展类 2、python对象的引用计数
   一般来说,在开发者是不直接管理对象使用的内存的,python是通过引用计数来决定一个对象使用的内存是否释放。所以在python中最可能的问题就是引用计数出错。    实际上很多语言(尤其是脚本语言)在内存管理上都是“引用计数<-->内存自动回收”的机制解决内存指针问题,在实现这种机制时,比较麻烦的 问题就是循环引用。在python中通过使用cycle detector来检测和回收循环引用的对象,但是如果对象定义了__del__钩子,自动循环引用机制就会失效,不过python通过gc module来检测循环应用的情况,gc module是一个可选的模块,在编译的时候可以指定是否把这个模块加入到python中。    引用计数是一个非常重要的概念,它是和使用环境密切相关的的,在python中(c环境中)存在三个和引用计数相关的概念:
   new reference:新的引用,它在"创建"对象的时候产生(这里说的"创建"也包括从cashe中获得),它表示在这个引用的上下文拥有这个引用的全新 的拷贝,如果上下文不再需要这个引用,必须使用Py_INCREF()减少引用计数,对于返回到python解析器的引用都必须是new reference,new reference又可以称为 owned reference    borrow reference:借来的引用,它一般用于函数参数的传递,它表示上下文可以在这个对象的引用计数变为0之前可以随意使用这个对象,它减少了引用计数的 烦恼,但是它会产生“悬浮”的引用,所以使用者必须保证使用的时候引用是有效的。可以通过Py_INCREF()将borrow reference转变为owned reference。但是bororw reference也有一些需要注意的地方:1、当删除一个对象的时候可能由于相互引用的关系,间接的删除了一个将要使用的borrow reference对象,所以如果要做删除前,最好要对将要使用的对象进行Py_INCREF()操作,在使用后进行Py_DECREF()操作;2、虽 然python中对全局的对象池是串行访问的,但是在多线程的c环境中可以通过Py_BEGIN_ALLOW_THREADS释放这个锁,所以也会有可能 造成"悬空"的borrow reference。    steal refercece:这种引用使用的情况很少,其实它不是描述引用本身,更确切的说是描述一个函数对引用的使用方式,它也是使用在函数参数传递的的情况 下,如:PyTuple_SetItem、PyList_SetItem。这些函数要求(或者说假设)传递的参数是一个owned reference(当然如果开发者坚持传递一个borrow reference的话,它只是会造成"悬空"的应用而已^_^)    通过c写python的扩展最令人头疼就是这几种引用和python c api中对引用的使用假设了。    这里总结一些基本的原则和特例,具体的函数在使用前最好还是查一查api的说明:
   2.1、通常函数的放回值是一个owned reference,例如:PyInt_FromLong() 、Py_BuildValue()、PySequence_GetItem()。 特例:PyList_GetItem()、PyTuple_GetItem(),PyDict_GetItem()、 PyImport_AddModule() 和 PyDict_GetItemString() 它们返回的是borrow reference    2.2、函数的参数一般是borrow reference ,例如;PyDict_SetItem()、PySequence_SetItem。特例:PyList_GetItem()、 PyTuple_GetItem(),它们要求参数是owned/steal reference,    2.3、返回到python解析器的基本都是 owned reference 3、c扩展类的信息注册
   从外部看来,一个c写的扩展类最后会以共享库的形式出现(当然,也可以和python的源码重新进行静态编译),windows下是dll(后缀会被改成 pyd),unix/linux平台就是so库,那么python是怎么区别这个库和别的库的区别,又怎么得到这个库中的定义的类型信息呢?    这很容易让人想起“注册-->回调”的工作方式,也就是设计一个固定的函数,在解析器加载该模块的时候自动寻找并调用这个函数,而在这个固定的函数 中在注册一些这个模块的信息。python使用的就是这种方式,不过它不是使用固定的函数名,而是是使用"initNAME"的方式确定初始化函数的名 称,NAME就是模块的名称。    前面已经提到模块的命名规则,当在python的解析器中“import mtest”的时候,python就寻找mtest.so中的initmtest进行调用模块的初始化。这个初始化的过程包括函数的注册,这就需要一个固 定的数据结构来描述函数的信息,在python中是通过static PyMethodDef mtestMethods[]来实现,这个结构每个成员是一个三元的数组{"func name", function, parameter type, "method doc"},作为结束标志,需要一个{ NULL, 0, NULL} 来表示结束。    在initNAME函数中一般是通过Py_InitModule("mtest", mtestMethods)把模块的信息注册到python中。可以看到,在python中一个类和一个函数的注册方式是一样的,不同的是??还有一点需 要注意的是在一个进程的c环境中,重复调用initNAME会产生问题,所以扩展类的开发者需要自己管理这些信息 4、在Unix环境扩展模块的安装
   3.1 将mtestmodule.c 和mtestmodule.h 放在python源码的Modules/目录下
   3.2 将Modules/Setup.local文件中增加一行:mtest mtesmodule.o
   3.3 configue --> make install (在windows下直接编译成pyd就可以了,当然出口函数时需要制定,也可以使用MSVC向导来确定出口函数) 5、python的错误处理模式
   在python中,异常是通过三个全局变量来保存的(对应于sys.exc_type, sys.exc_value 和sys.exc_traceback ),事实上python解析器中绝大部分的语法规则都会导致函数调用,在这些函数如果发生了异常就是通过返回NULL(它和None对象不同)给调用 者,python中对于参数的处理都不检查合法性(NULL检查),这是基于效率考虑的,同样的Py_INCREF()、Py_DECREF()也不检 查,而Py_XINCREF()、Py_XDECREF()是检查的,而这两个函数一般只是用在dealloc函数中。
   在c的api中已经提供了很多有用的设置异常信息的函数:PyErr_SetString(), PyErr_SetObject(), PyErr_SetFromErrno()。有个需要注意的地方是传递给这些函数的obj不需要Py_INCREF()。如果在c程序中需要检测错误,可 以使用PyErr_Occurred(). 而PyErr_Clear()可以清楚已经发生的异常。一般来说,异常的处理是又最先发现异常的函数来设置异常的信息,由python解析器来处理这些异 常,而中间的函数只是简单地把NULL值或者-1返回给调用者。    因为内存分配是非常频繁的操作,所以PyErr_NoMemory() 是直接由所有具有创建对象的函数调用的,对于开发者来说这个函数很少使用,除非开发者需要自己管理创建对象的内存分配。    还有一点需要注意的是在处理内存的时候,需要处理一些已经创建对象的引用计数(Py_XDECREF() 或 Py_DECREF() )当然,开发者也可以通过PyErr_NewException()函数自定义异常,并使用PyModule_AddObject()把这中类型注册到 build-in模块中。这里有一个需要注意的是对于返回到python解析器的对象,或者注册到系统的对象都要先增加引用计数(Py_INCREF) 6、c环境调用python定义的函数
   同样地,如果c换数要调用python定义的函数/模块,也是通过“注册回调的方式”,流程是这样的
   python调用c的一个“普通的注册函数”-->c函数把传递过来的对象记录下来(注意引用计数的)-->在需要的时候通过PyEval_CallObject()调用这个注册函数    python中注册的“回调”函数通常是使用METH_VARARGS作为函数的定义,而在c环境中调用这个python定义的函数的时候就把函数指针和 args传递给PyEval_CallObject进行处理,PyEval_CallObject对参数的引用计数是不影响的,也就是说参数的引用计数必 须由调用者管理。 7、c环境的模块函数共享
   为了在c层次上提供扩展模块的函数共享,python抽象出“PyCObject”,它其实是一个void* 的封装,用途是在一个模块中绑定一个用户定义的指针,如果这个指针指向的是模块“export”函数的地址的话,在不同的扩展模块中就能共享函数,这种机 制提供了在扩展类中“export”函数的共享: python基础之c扩展类    这里需要注意的是,转化为Cobject的指针是用户自定义的,也就是说它可以python对象,也可以是纯粹的内存数据。而且 function_porter是一个函数的指针,它在Cobject回收的时候调用,一般来说它执行资源的释放操作,一般来说,如果只是进行函数共享的 话,只要设置为NULL即可。    同时为了方便使用,一般来说会把加载模块和数据提取的代码通过条件编译直接写在头文件的一个函数中,而且把这个函数也按照一定的规则命名,这样使用者就可以方便使用了。 8、自定义对象
   了解了用c写扩展模块的基本概念后,下面看看自定义对象的编写。
   python中所有的东西都是对象,但是c语言却不支持面向对象的,现在我们来看看是怎么实现python的对象模式的。
   python中的类是通过struct来模拟的,所以要定义一个类就需要实现填充一个结构体,这个结构体就是PyTypeObject,它在 object.h中定义,描述了类的类型方法“type method”, 同时还要定义另外一个结构表示实例的成员变量,事实上这个描述实例的结构体在类型定义中一方面提供了一个sizeof()的内存大小计算,另一方面在构造 tp_members的时候计算成员数据的内存地址偏移,也就是说这个结构体是为了设计类实例的内存布局而存在的。
python基础之c扩展类    经过这样的处理后,在python解析器中就能够使用新的类型了:
   import module  
   obj = module.class() 为了把源代码编译为共享库,除了前面提到的方法外,还可以使用python的工具包进行编译:
   from distutils.core import setup, Extension
   setup(name="mtest", version="1.0",
      ext_modules=[
         Extension("mtest", ["mtestmodule.c"]),
         ]) 定义成员函数是通过PyMemberDef结构体实现的,PyMemberDef 的结构如下:
static PyMemberDef mtest_members[] = {
    {"number", T_INT, offsetof(module_classObject, number), 0,
     "mtest number"},
     {"name", T_OBJECT_EX, offsetof(module_classObject, name), 0,
     "mtest name"},
    {NULL} /* Sentinel */
}; 通过PyMemberDef定义的PyObject*成员变量的值的改变是python自动进行的,也就是说用户不能够进行设置的类型的检测,在使用者的 角度看,一个语义上表示名字的python string变量完全可以表示一个list。为了进行更精确的类型判断,我们可以设置descriptor:
static PyGetSetDef mtest_getseters[] = {
    {"name",
     (getter)mtest_getname, (setter)mtest_setname,
     "name",
     NULL},-->这个是闭包的指针,用于保存getter/setter额外的数据,使用得比较少
    {NULL} /* Sentinel */ 由于descriptor和属性是对应的,所以可以针对特别的成员变量进行类型检查,当然开发者也可以在tp_getattr/tp_setattr中进 行检查,只是这样逻辑会集中在一起,代码可能比较长。使用descriptor还有一个需要注意的地方是它不是一个成员变量,也就是它不是一个常规的成员 变量,我们必须把它的定义从PyMemberDef中移除, 而且在tp_init中解析参数的时候也只需要把这个descriptor参数的类型改成需要的类型即可,例如:
PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist, &tmp_name, &self->number))
==>>PyArg_ParseTupleAndKeywords(args, kwds, "|SSi", kwlist, &tmp_name, &self->number))    使用“引用计数”的内存管理模式的虚拟机都需要考虑“内存”回收的问题,在内存回收时就需要考虑循环引用的情况。如果扩展模块需要支持循环循环引用的垃 圾回收,就需要定义tp_traverse和tp_clear钩子函数,同时还要在tp_flags指定Py_TPFLAGS_HAVE_GC标志
   在tp_traverse函数的原型是:traverse(Noddy *self, visitproc visit, void *arg)
   在这个函数中我们需要对可能参与循环应用的对象(也就是PyObject* 的变量调用 visit),并把非0的放回值返回到python:    if (self->name) {
        vret = visit(self->name, arg);
        if (vret != 0)
            return vret;    ...
也可以使用宏Py_VISIT(self->name)完成这个功能,而在tp_clear函数中就需要减少可能参与循环引用的对象进行 Py_XDECREF,但是需要注意的是它需要先把这个对象赋值给一个临时变量,这是因为Py_XDECREF可能会触发“垃圾收集”流程,而在这个流程 中又检查正在删除的对象,但事实上这个对象(或者说引用已经"悬空"了),这样就会导致python崩溃: tmp = self->name;
self->name = NULL;
Py_XDECREF(tmp); 当然可以使用宏Py_CLEAR来实现这些细节。 现在我们已经知道了用c写python扩展类的基本知识和流程,这里再总结一下在处理一个类型的钩子函数时需要注意的细节: 8.1、tp_dealloc钩子
     这个钩子函数是在一个对象的应用计数变成0的时候调用的,它是为了给设计者一个处理自管理资源的释放,但是它并不检查或处理悬挂的异常信息(这种设计), 如果在这个函数中进行的处理激发了一个检查异常的函数,就会给python解析器造成混乱,或者是信息的丢失。为了保存这些信息,可以使用 PyErr_Occurred()检查异常是否发生,PyErr_Fetch()提取异常信息,PyErr_Restore()恢复异常信息。 8.2、信息输出函数
     tp_repr:输出一个用于标识对象信息的string,在repr(obj)的时候调用,对于标准对象,可以使用eval(repr(obj))来得到原来的对象,但是对于自定义对象,通常是不成立的。
     tp_str:输出一个能方便阅读的字符串,在str(obj)的时候调用,如果这个钩子不存在,那么就会调用repr
     tp_print:通常它和str是一样的,但是它是不构建string作为一个新对象,并对这个对象进行输出操作,它是直接把内容写到输出上,事实上可以把它理解为"流"输出,在严格考虑效率的环境,可以使用tp_print 8.3、属性解析方式
     在python中有两种方式进行属性的查询:
     1、通过在PyTypeObject结构体中指定tp_methods/tp_members/tp_getset来定制属性访问的方式,这种方式适合用在定义类的时候就知道该类型的成员的信息
     2、通过提供tp_getattr/tp_setattr(tp_getattro/tp_setattro)钩子,它在python访问对象不存在的属 性时被调用,这种方式适合处理动态属性的情况,对于静态属性,这种方式还能够兼容python2.2以下的版本。
    一般来说,使用tp_getattr/tp_setattr钩子需要定义一个函数的映射表,然后在钩子函数中,根据属性名查询对应的处理函数:
        static PyMethodDef exm_attrs_methods[] = {
                {"getName", (PyCFunction)exm_attrs_getname, METH_VARARGS,
                 "get the name"},
                {"setName", (PyCFunction)exm_attrs_setname, METH_VARARGS,
                 "Set the name."},
                {NULL, NULL, 0, NULL}           /* sentinel */     可以看到,这个描述处理函数的"表"和定义tp_methods的格式是一样的,只是它是作为一个独立的变量保存信息,而在tp_getattr钩子中就会使用Py_FindMethod查找并调用处理函数.
        static PyObject *
        tp_getattr(newdatatypeobject *obj, char *name)             return Py_FindMethod(exm_attrs_methods, (PyObject *)obj, name);   
   8.4、几个常用的钩子函数
    tp_call:这个钩子在把对象用作函数的时候被调用,它接受3个参数,第一个是调用的对象,第二个是位置参数*args,第三个是字典参数**kw
    tp_iter:如果类型支持枚举操作,就需要实现tp_iter钩子,这个钩子需要返回一个可枚举的对象,也就是这个可枚举的对象必须同时实现 tp_iter和tp_iternext钩子。对于支持多次枚举的对象,在tp_iter中需要创建一个新的iterator object,而只支持一次枚举的对象只需要增加自己的引用计数就可以了。而iterator object需要同时实现tp_iter 和tp_iternext钩子,在tp_iter中需要增加自己的引用计数,而在tp_iternext中就需要增加返回对象的引用计数,并在到达结尾的 时候设置StopIteration异常。iterator object还需要维护它自身的内部状态。
9、整体感觉
   1、使用c写python的扩展模块的流程是很规范的,但它的引用计数规则和内存指针的直接管理本质上没有区别,对于程序员来说,还是需要考虑"悬空"和"泄漏"的问题,只是现在考虑的是"引用"(这也是写c程序必须考虑的事情^_^)。    2、在进行Py_DECREF()操作的时候要考虑是否会间接造成将要使用对象释放,并时刻留意各种reference之间的转变。    (这篇blog是python标准文档的整理)
顶一下
(1)
踩一下
(0)