使用C/C++扩展Python之二

在 Thu 26 November 2015 发布于 Python 分类

Python 引用计数 在上一篇文章中,简单的介绍了使用C/C++开发Python模块的一般步骤。还借用上一篇文中的代码:

static PyObject* py_reverse(PyObject* self, PyObject* args)
{
    char* result;
    char* target = NULL;
    PyObject* retval = NULL;

    int ret = PyArg_parseTuple(args, "s", &target);
    if (!ret) {
        return NULL;
    }   

    result = reverse(target);
    retval = (PyObject*)Py_BuildValue("s", result);
    return retval;
}

上篇文章说道,这只是一个很简陋的封装,还缺少错误处理和引用计数处理的逻辑。 Python是动态语言,有自己一套垃圾处理。Python的垃圾处理主要依靠引用计数。这个概念和c++11的智能指针类似,Python的对象结构体中都存在一个ob_refcnt字段,这个字段代表的就是引用计数。当ob_refcnt的值为0的时候,就会调用对象的元信息中的删除钩子函数把该对象所占用的资源释放掉。如果ob_refcnt的值一直都不为0, 那就有可能造成内存泄漏了,当这样的对象逐渐多起来的时候,内存就会慢慢吃紧。Python提供了一系列的预定义宏来操作ob_refcnt。常用的就属Py_INCREF()Py_DECREF这两个宏,但是使用这两个宏需要注意的是其操作的PyObject*不能为NULL,Py_XINCREF()Py_XDECREF就没有这个限制。

下面,总结了几条怎样使用Py_INCREFPy_DECREF的方法:

  1. 创建的Python对象(涉及到分配内存‘new’操作)如果不通过返回值或者参数列表传递出去该对象所处的函数作用域,都应该使用Py_DECREF,来使ob_refcnt减1,因为Python对象都是动态创建的,创建后ob_refcnt的值就是1,代表着对象所在的函数作用域享有这个对象, 在函数执行完成后,该对象应该随着函数的完成被Python的垃圾回收机制给回收回去。
  2. 对于通过Python API获取到的内存地址,已经创建过的Python对象, 此时需要使用Py_INCREF来宣布该对象所在的函数作用域对其的享有,但是需要在离开作用域之前使用Py_DECREF来回收该对象所在的函数作用域对其的享有。

在上面的代码中,显然是不需要使用Py_INCREFPy_DECREF的。虽然类型为PyObject*的变量result让该对象所处的作用于对其享有,但是随着函数的返回,这个对象别传递出去了,其所有权交给了Python的运行环境。

自定义异常 在使用C/C++来做扩展Python时,可以自定义异常。首先需要定义一个编译单元内的静态全局的变量,类型为PyObject*

static PyObject* reverseError;

在初始化模块的时候,给扩展模块reverse模块在添加一个异常类error,

PyMODINIT_FUNC initreverse(void) {
    PyObject* m;
    m = Py_InitModule3("reverse", reverse_methods, "My first extension module.");
    if (m == NULL) {
        return NULL;
    }

    reverseError= PyErr_NewException("reverse.error", NULL, NULL);
    Py_INCREF(SpamError); #防止对象在别的作用域下被执行了Py_DECREF,而被垃圾回收
    PyModule_AddObject(m, "error", reverseError); #把该异常对象加入到模块
}

在初始化模块的时候

PyMODINIT_FUNC initreverse(void) {
    PyObject* m;
    m = Py_InitModule3("reverse", reverse_methods, "My first extension module.");
    if (m == NULL) {
        return NULL;
    }

    reverseError= PyErr_NewException("reverse.error", NULL, NULL);
    Py_INCREF(SpamError);
    PyModule_AddObject(m, "error", reverseError);
}
static PyObject* py_reverse(PyObject* self, PyObject* args)
{
    char* result;
    char target[1024] = {0};
    PyObject* retval = NULL;

    int ret = PyArg_parseTuple(args, "s", &target);
    if (!ret) {
        PyErr_SetString(reverseError, "reverse failed");
        return NULL;
    }   

    result = reverse(target);
    retval = (PyObject*)Py_BuildValue("s", result);
    return retval;
}

修改完成后编译和安装扩展模块,然后测试一下:

>>> import reverse
>>> reverse.reverse(1111)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
reverse.error: reverse failed
>>>

当然,上面的代码,添加的异常很不合适,很明显是格式化的类型不正确,应该使用其默认的异常处理,即TypeErrorPython API提供的很多的异常对象,具体可以看Python的官方文档。 在上面的封装的代码片段中,使用的是以PyErr_开头的函数来出发异常。需要注意的是在多层嵌套调用下,某一层被调用的函数如果产生异常并调用了PyErr_*来触发异常,并返回NULL或者-1,调用层函数的函数不应该在继续调用PyErr_*来触发异常了,而应该返回NULL或者-1,依次类推。如果不想触发异常的化,调用PyErr_Clear函数。