python c++

最近为了实现某个功能,分享一下写 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

点击复制链接 与好友分享!回本站首页

 

 

=============================================================================================================================================================================================================================================================================================================================================================================================================================================================

 

 

扩展Python模块系列(四)----引用计数问题的处理

      承接上文,发现在使用Python C/C++ API扩展Python模块时,总要在各种各样的地方考虑到引用计数问题,稍不留神可能会导致扩展的模块存在内存泄漏。引用计数问题是C语言扩展Python模块最头疼的地方,需要由程序员对使用的每个C API都要充分了解,甚至要熟悉源码才能精确掌握什么时候引用计数加一,什么时候减一。

  本文为翻译文章,我觉得对于源码中的引用计数讲解得比较清楚,所以就翻译为中文。http://edcjones.tripod.com/refcount.html#

Summary:

 Python Object的结构体定义包含一个引用计数和对象类型:

复制代码

#define PyObject_HEAD \
             int ob_refcnt; \
             struct _typeobject *ob_type;

     typedef struct _object {
             PyObject_HEAD
     } PyObject;

复制代码

 

    Python提供了两组与引用计数相关的宏定义【object.h】: 

#define Py_INCREF(op) (                                 \
        _Py_CHECK_THREAD_SAVE                           \
        _Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA            \
        ((PyObject*)(op))->ob_refcnt++)

复制代码

/*当引用计数为0时,会释放对象所占的内存*/
#define Py_DECREF(op)                                   \
    do {                                                \
        if (_Py_CHECK_THREAD_SAVE                       \
            _Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA        \
            --((PyObject*)(op))->ob_refcnt != 0)        \
            _Py_CHECK_REFCNT(op)                        \
        else                                            \
            _Py_Dealloc((PyObject *)(op));              \
    } while (0)

复制代码

另外一组考虑是对象为NULl的情况:

#define Py_XINCREF(op) do { if ((op) == NULL) ; else Py_INCREF(op); } while (0)
#define Py_XDECREF(op) do { if ((op) == NULL) ; else Py_DECREF(op); } while (0)

 在Python中,没有谁能真正拥有一个对象,只拥有对象的引用。一个对象的reference count定义为该对象的引用者数量,对象的引用者当不再使用该对象时有责任主动调用Py_DECREF(),当reference count为0时,Python可能会delete这个对象。

每次调用Py_INCREF(),最终都应该对应调用Py_DECREF()。C语言中,每个malloc,必须最终调用free()。而现实很容易忘记free掉在堆上分配的内存,而且不使用工具的话也难以察觉内存泄漏问题,因为现代机器内存、虚拟内存都很充足,一般会在长时间运行的服务器程序上出现内存泄漏问题。

当一个指向Python Object的指针的引用计数加1,就是说这个对象被protected.

什么时候调用Py_INCREF()和Py_DECREF()

从 函数中返回一个Python Object

大部分Python 对象是通过Python C API提供的函数来创建的,一般形如 PyObject* Py_Something(arguments)创建一个Python 对象,然后返回给调用者。一般在Py_Something函数中对该Python对象调用了Py_INCREF(并不是所有的函数都会调用),而调用Py_Something的函数在使用其返回的Python对象时要牢记该对象引用计数已被加1,当不再需要该对象时需要调用Py_DECREF()。

 

 void MyCode(arguments) {
         PyObject* pyo;
         ...
         pyo = Py_Something(args);

 

MyCode函数调用了Py_Something,有责任处理pyo的引用计数,当MyCode使用完了pyo之后,必须要调用Py_DECREF(pyo)。

不过,如果MyCode需要返回pyo对象,比如:

复制代码

 PyObject* MyCode(arguments) {
         PyObject* pyo;
         ...
         pyo = Py_Something(args);
         ...
         return pyo;
     }

复制代码

此时,MyCode不应该调用PY_DECREF(),在这种情况下,MyCode将pyo对象的引用计数责任传递了出去。

Note:如果一个函数返回的是None对象,C代码应该是这样:必须要增加None对象的引用计数。

    Py_INCREF(Py_None);
    return Py_None;

到目前为止讨论了最常见的情况,即当调用Py_Something创建了一个引用,并将引用计数的责任传递给其调用者。在Python文档中,这被称为new reference。比如文档中有说明:

  PyObject* PyList_New(int len)
           Return value: New reference.
           Returns a new list of length len on success, or NULL on failure.

当一个引用被INCREF,通常称为这个引用被protected。

有时候,Python源码中不会调用Py_DECREF()。 

复制代码

  PyObject *
     PyTuple_GetItem(register PyObject *op, register int i)
     {
             if (!PyTuple_Check(op)) {
                     PyErr_BadInternalCall();
                     return NULL;
             }
             if (i < 0 || i >= ((PyTupleObject *)op) -> ob_size) {
                     PyErr_SetString(PyExc_IndexError,
                              "tuple index out of range");
                     return NULL;
             }
             return ((PyTupleObject *)op) -> ob_item[i];
     }

复制代码

这种情况被称为borrowing a reference。

 PyObject* PyTuple_GetItem(PyObject *p, int pos)
          Return value: Borrowed reference.
          Returns the object at position pos in the tuple pointed to by
          p.  If pos is out of bounds, returns NULL and sets an
          IndexError exception.

本文也称之为这个对象的引用是unprotected。

Python源码中,返回unprotected preferencess(borrowing a reference)的函数有:

复制代码

PyTuple_GetItem(),
PyList_GetItem(),
PyList_GET_ITEM(),
PyList_SET_ITEM(),
PyDict_GetItem(),
PyDict_GetItemString(),
PyErr_Occurred(),
PyFile_Name(),
PyImport_GetModuleDict(),
PyModule_GetDict(),
PyImport_AddModule(),
PyObject_Init(),
Py_InitModule(),
Py_InitModule3(),
Py_InitModule4(), and
PySequence_Fast_GET_ITEM().

复制代码

对于PyArg_ParseTuple()来说,这个函数有时候会返回PyObject,存在类型为PyObject*的参数中。比如sysmodule.c中的例子:

复制代码

 static PyObject *
     sys_getrefcount(PyObject *self, PyObject *args)
     {
             PyObject *arg;
             if (!PyArg_ParseTuple(args, "O:getrefcount", &arg))
                     return NULL;
             return PyInt_FromLong(arg->ob_refcnt);
     }

复制代码

PyArg_ParseTuple源码的实现中,没有对arg的引用计数INCREF,所以arg是一个unprotected object,当sys_getrefcount返回时,arg不应当被DECREF。

这里提供一个比较完整的功能函数,计算一个列表中的整数之和.

示例1:

复制代码

     long sum_list(PyObject *list)
     {
         int i, n;
         long total = 0;
         PyObject *item;

         n = PyList_Size(list);
         if (n < 0)
             return -1; /* Not a list */
             /* Caller should use PyErr_Occurred() if a -1 is returned. */
         for (i = 0; i < n; i++) {
             /* PyList_GetItem does not INCREF "item".
                "item" is unprotected. */
             item = PyList_GetItem(list, i); /* Can't fail */
             if (PyInt_Check(item))
                 total += PyInt_AsLong(item);
         }
         return total;
     }

复制代码

PyList_GetItem()返回的item是PyObject类型,引用计数没有被INCREF,所以函数done之后没有对item进行DECREF。

示例2:

复制代码

   long sum_sequence(PyObject *sequence)
     {
         int i, n;
         long total = 0;
         PyObject *item;
         n = PySequence_Length(sequence);
         if (n < 0)
             return -1; /* Has no length. */
             /* Caller should use PyErr_Occurred() if a -1 is returned. */
         for (i = 0; i < n; i++) {
             /* PySequence_GetItem INCREFs item. */
             item = PySequence_GetItem(sequence, i);
             if (item == NULL)
                 return -1; /* Not a sequence, or other failure */
             if (PyInt_Check(item))
                 total += PyInt_AsLong(item);
             Py_DECREF(item);
         }
         return total;
     }

复制代码

与示例1不同,PySequnce_GetItem()的源码实现中,对返回的item的引用计数进行了INCREF,所以在函数done时需要调用Py_DECREF(item)。

什么时候不需要调用INCREF

1.对于函数中的局部变量,这些局部变量如果是PyObject对象的指针,没有必要增加这些局部对象的引用计数。理论上,当有一个变量指向对象的时候,对象的引用计数会被+1,同时在变量离开作用域时,对象的引用计数会被-1,而这两个操作是相互抵消的,最终对象的引用数没有改变。使用引用计数真正的原因是防止对象在有变量指向它的时候被提前销毁。

什么时候需要调用INCREF

如果有任何的可能在某个对象上调用DECREF,那么就需要保证该对象不能处于unprotected状态。

1) 如果一个引用处于unprotected,可能会引起微妙的bug。一个常见的情况是,从list中取出元素对象,继续操作它,但是不增加它的引用计数。PyList_GetItem 会返回一个 borrowed reference ,所以 item 处于未保护状态。一些其他的操作可能会从 list 中将这个对象删除(递减它的引用计数,或者释放它)。导致 item 成为一个悬垂指针。

复制代码

bug(PyObject *list) {
         PyObject *item = PyList_GetItem(list, 0);

         PyList_SetItem(list, 1, PyInt_FromLong(0L));
         PyObject_Print(item, stdout, 0); /* BUG! */
     }

复制代码

这个函数的功能:从list中取出第0个元素item(此时没有递增它的引用计数),然后替换list[1]为整数0,最后打印item.看起来很正常,没有什么问题,其实不然。

我们跟着PyList_SetItem函数的流程走一遍。list中所有元素的引用计数都是protected的,所以当把list[1]的元素替换时,必须将原来的元素的引用计数减少。假设原来的元素list[1]是一个用户自定义的一个类,并且实现了__del__方法。如果这个类的instance的引用计数为1,当减少它的引用计数时,此instance会被释放,会调用__del__方法。而__del__方法是python用户自己写的代码,所以__del__可以是任意的python代码,那么是不是有可能做了某些操作导致list[0]的引用计数无效,比如在__del__方法中del list[0],假如list[0]的引用计数也是1,那么list[0]会被释放,而被释放的item再次被作为参数传递给了PyObject_print()函数,此时会出现意想不到的行为。

解决的办法也很多简单:

复制代码

    no_bug(PyObject *list) {
         PyObject *item = PyList_GetItem(list, 0);
         Py_INCREF(item); /* Protect item. */

         PyList_SetItem(list, 1, PyInt_FromLong(0L));
         PyObject_Print(item, stdout, 0);
         Py_DECREF(item);
     }

复制代码

 This is a true story. An older version of Python contained variants of this bug and someone spent a considerable amount of time in a C debugger to figure out why his __del__() methods would fail...

2) 传递PyObject对象给函数,一般都是假设传递过来的对象的引用计数已经是protected,因此在函数内部不需要调用Py_INCREF。不过,如果想要参数存活到函数退出,可以调用Py_INCREF。

When you pass an object reference into another
    function, in general, the function borrows the reference from you
    -- if it needs to store it, it will use Py_INCREF() to become an
    independent owner.

PyDict_SetItem()就是这样的例子,将某些东西存放在字典中,会将key和value的引用计数都加1.

 

而PyTuple_SetItem()和PyList_SetItem()与PyDict_SetItem()不同,他们接管传递给他们的对象(偷取一个引用)。

PyTuple_SetItem的原型是PyTuple_SetItem(atuple, i, item): 如果atuple[i]当前包含了一个PyObject,则将此PyObject DECREF,然后atuple[i]设置为item。 item并不会被INCREFed

如果PyTuple_SetItem插入元素失败,会减少item的引用计数。同样,PyTuple_GetItem不会增加返回的item的引用计数。

PyObject *t;
PyObject *x;
x = PyInt_FromLong(1L);
PyTuple_SetItem(t, 0, x);

当x作为参数传递给PyTuple_SetItem函数时,那么必须不能调用Py_DECREF,因为PyTuple_SetItem()函数实现中没有增加x的引用计数,如果你此时人为减少x的引用计数,那么tuple t中的元素item已经被释放了。

当tuple t 被DECREFed,其里面的元素都会被DECREFed。

PyTuple_SetItem这样的设计主要是考虑到一个很常见的场景:创建一个新的对象来填充tuple或list。例如创建这样一个tuple, (1, 2, "there")。 使用Python C API可以这样做:

复制代码

  PyObject *t;

     t = PyTuple_New(3);
     PyTuple_SetItem(t, 0, PyInt_FromLong(1L));
     PyTuple_SetItem(t, 1, PyInt_FromLong(2L));
     PyTuple_SetItem(t, 2, PyString_FromString("three"));

复制代码

Note: PyTuple_SetItem是设置tuple元素的唯一的方法。 PySequence_SetItem和PyObject_SetItem都会拒绝这样做,因为tuple是一个不可变的数据类型。

创建list与创建tuple的接口类似,PyList_New()和PyList_SetItem(),有个区别是填充list的元素可以使用PySequence_SetItem(),但是PySequence_SetItem会增加传入的item的引用计数。

复制代码

PyObject *l, *x;

 l = PyList_New(3);
 x = PyInt_FromLong(1L);
 PySequence_SetItem(l, 0, x); Py_DECREF(x);
 x = PyInt_FromLong(2L);
 PySequence_SetItem(l, 1, x); Py_DECREF(x);
 x = PyString_FromString("three");
 PySequence_SetItem(l, 2, x); Py_DECREF(x);

复制代码

 

Python信奉极简主义,上述创建tuple(list)和填充tuple(list)的代码可以简化为:

 PyObject *t, *l;

 t = Py_BuildValue("(iis)", 1, 2, "three");
 l = Py_BuildValue("[iis]", 1, 2, "three");

 

Two Examples:

Example 1:

 

复制代码

    PyObject*
    MyFunction(void)
    {
        PyObject* temporary_list=NULL;
        PyObject* return_this=NULL;

        temporary_list = PyList_New(1);          /* Note 1 */
        if (temporary_list == NULL)
            return NULL;

        return_this = PyList_New(1);             /* Note 1 */
        if (return_this == NULL)
            Py_DECREF(temporary_list);           /* Note 2 */
            return NULL;
        }

        Py_DECREF(temporary_list);               /* Note 2 */
        return return_this;
    }

复制代码

Note1: PyList_New返回的object的引用计数为1

Note2: 因为temporary_list 在函数退出时不应该存在,所以在函数返回前必须DECREFed。

 

Example 2:

复制代码

    PyObject*
    MyFunction(void)
    {
        PyObject* temporary=NULL;
        PyObject* return_this=NULL;
        PyObject* tup;
        PyObject* num;
        int err;

        tup = PyTuple_New(2);
        if (tup == NULL)
            return NULL;

        err = PyTuple_SetItem(tup, 0, PyInt_FromLong(222L));  /* Note 1 */
        if (err) {
            Py_DECREF(tup);
            return NULL;
        }
        err = PyTuple_SetItem(tup, 1, PyInt_FromLong(333L));  /* Note 1 */
        if (err) {
            Py_DECREF(tup);
            return NULL;
        }

        temporary = PyTuple_Getitem(tup, 0);          /* Note 2 */
        if (temporary == NULL) {
            Py_DECREF(tup);
            return NULL;
        }

        return_this = PyTuple_Getitem(tup, 1);        /* Note 3 */
        if (return_this == NULL) {
            Py_DECREF(tup);
            /* Note 3 */
            return NULL;
        }

        /* Note 3 */
        Py_DECREF(tup);
        return return_this;
    }

复制代码

Note1:如果PyTuple_SetItem失败或者这个tuple引用计数变为0,那么PyInt_FromLong创建的对象引用计数也被减少

Note2:PyTuple_GetItem不会增加返回的对象的引用计数

Note3:MyFunction没有责任处理temporary的引用计数,不需要DECREF temporary

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值