python module 小结


最近为了实现某个功能,分享一下写 python module 的一些心得。这里主要是指,使用 C/C++ 实现一个 module 而不是用 python 语言本身。

module 的结构

一个简单的 module 包含一系列 static 函数,这些函数的作用是将 python 能够 handle 的 PyObject 转换成为 C/C++ 代码能够 handle 的类型,比如我们写了一堆算法,是基于 boost.numeric.ublas 的,因此我们已经实现的操作对象是 boost::numeric::matrix<double>,通过一个简单的策略,我们将两者的指针进行转换(一般 PyObject 实现了一个计数器,提供给 garbage collection 使用),这样我们就能利用原先实现的代码完成需要的功能,最后再转换成为 PyObject 返回给 python。

其中,为了标识每个 PyObject 的类型,往往会定义一个 PyTypeObject 关联到对应 PyObject 上,这个 type object 实际上包含了创建、销毁、显示以及其他相关对这类对象的操作。为了避免不同 module 之间符号表的冲突,python 建议这些函数均写为 static 类型,仅仅有一个导出 module 信息的函数不是,这个函数初始化整个 module 暴露给 python 的接口,比如提供了什么类型,有些什么方法等等。暴露给 python 的一个标准方法必须返回 PyObject*,如果返回 NULL 就会抛出对应的异常。

具体的例子这里就不写了,可以参考这里。初始化函数的流程大约是将 static 成员初始化(如果有特殊的需求的话),然后将需要 export 的函数用 Py_InitModule 注册。

一个矛盾的用例

比较麻烦的事情出现在如果你有一个 module,另外一个 module 希望与其 share 某个类型,比如 opencv 提供了一个 python module,里面为 CvMat 做好了封装,这使得我们可以直接通过 python 读取图片进行处理,可是如果你写了一个 C/C++ 的 library,也是基于 opencv 的,你希望在你的 module 里面能够拿到 opencv 提供的封装好的 CvMat 对象并进行处理、返回怎么办呢?

自然的想法就是如果 opencv 提供了相关的 header,我们编译的时候使用它们,之后链接到 cv2.so 上就行了。但是这个并不 work,那个 static 关键字禁止其他的编译单元对其进行链接。我们是否可以在不修改原有 module 的情况下实现这个功能呢?

很自然的做法就是直接将对方 module 的相关声明 copy 到自己正在写的 module 里面(如果没有提供头文件),但是 PyTypeObject 这样弄过来却不行,主要原因是这个类型的对象在 opencv module 里面进行的初始化,我们并不是需要复制这个 PyTypeObject,我们就需要它本身(因为 type 作为 singlton 在程序中存在,type 决定是否为 subtype 的时候是直接对地址进行比较的,如果不对比较父类的地址直到结束这种比较)。因此我们可以简单的写一段 python,传递我们需要的对象,利用这个对象我们就能拿到对应的 PyTypeObject 类型了:

import cv2
import cvkit
im = cv2.imread ('some.pic')
cvkit.init (im)
cvkit.do_something (im)

也就是说我们的 package 需要提供一个 init 方法,专门用来获取 cv2 里面使用的 type,这样做并不合理,因为程序员可能会忘记调用 cvkit.init 而这会导致后面的调用不能正常运行。尽管如此,这种方法仍然是 work 的,下面是一段关于这个的 C++ 代码

static PyTypeObject *cvmat_Type = NULL ;

static PyObject*
init (PyObject* self, PyObject* args) {
  PyObject* img_pobj ;
  if (!PyArg_ParseTuple(args, &quot;O&quot;, &amp;img_pobj))
    return NULL ;
  cvmat_Type = img_pobj-&gt;ob_type ;
  Py_INCREF(Py_None);
  return Py_None;
}

那么更自然的方式就是在初始化我们的 module 的时候自动的实现这个过程,这可能吗?

看了看 python capi 的说明,下面的函数将帮助我们实现这个功能:

static PyTypeObject *cvmat_Type = NULL ;

static void
init (PyObject* pimg) {
  cvmat_Type = pimg-&gt;ob_type ;
  Py_INCREF(Py_None);
  return Py_None;
}

PyMODINIT_FUNC
initimghash(void) {
  PyObject *m, *nm, *pimg, *arg, *func, *dict;

  // load cv module
  nm = PyString_FromString (&quot;cv&quot;) ;
  if (nm == NULL)
    return ;
  m = PyImport_Import (nm) ;
  if (m == NULL)
    return ;

  // init the types
  Py_XINCREF (m) ;
  dict = PyModule_GetDict (m) ;
  func = PyDict_GetItemString (dict, &quot;LoadImageM&quot;) ;
  arg = PyTuple_New (1) ;
  PyTuple_SetItem (arg, 0, PyString_FromString (&quot;test.jpg&quot;)) ;
  pimg = PyObject_CallObject (func, arg) ;
  init (pimg) ;

  // release them
  Py_XDECREF (nm) ;
  Py_XDECREF (dict) ;
  Py_XDECREF (func) ;
  Py_XDECREF (arg) ;
  Py_XDECREF (pimg) ;

  // expose interface
  m = Py_InitModule(&quot;imghash&quot;, CvkitMethods);
  if (m == NULL)
    return;
  CvkitError = PyErr_NewException(&quot;cvkit.error&quot;, NULL, NULL);
  Py_INCREF(CvkitError);
  PyModule_AddObject(m, &quot;error&quot;, CvkitError);
}

这里通过 cvLoadImageM 获得的 type,还是很 ugly 的手法,一种更好的解决方案是利用 cv.cvmat 这个 PyTypeObject,可以在 cvkit 模块初始化阶段载入 cv 模块,并获取 cvmat 的指针赋值给 cvmat_

题外话

有人说那 opencv 如何与 numpy 交互的呢?我知道 PIL 和 numpy、opencv 与 numpy 似乎它们都可以互相转换,甚至 share 数据类型呢!就我看到的实现,他们并没有 share 这里所说的 PyTypeObject,事实上,它们更倾向使用某些“接口”,比如 tostring 或者 array。下面一段来自 opencv 的片段,

static PyObject *fromarray(PyObject *o, int allowND)
{
  PyObject *ao = PyObject_GetAttrString(o, &quot;__array_struct__&quot;);
  PyObject *retval;

  if ((ao == NULL) || !PyCObject_Check(ao)) {
    PyErr_SetString(PyExc_TypeError, &quot;object does not have array interface&quot;);
    return NULL;
  }
  PyArrayInterface *pai = (PyArrayInterface*)PyCObject_AsVoidPtr(ao);
  if (pai-&gt;two != 2) {
    PyErr_SetString(PyExc_TypeError, &quot;object does not have array interface&quot;);
      Py_DECREF(ao);
      return NULL;
  }
  // ...
}

 摘自 python.cn

相关内容

    暂无相关文章

评论关闭