使用Python C API时检查返回值有多重要?
看起来每次我调用一个返回 PyObject* 的函数时,都得加四行代码来检查错误。比如:
py_fullname = PyObject_CallMethod(os, "path.join", "ss", folder, filename);
if (!py_fullname) {
Py_DECREF(pygame);
Py_DECREF(os);
return NULL;
}
image = PyObject_CallMethodObjArgs(pygame, "image.load", py_fullname, NULL);
Py_DECREF(py_fullname);
if (!image) {
Py_DECREF(pygame);
Py_DECREF(os);
return NULL;
}
image = PyObject_CallMethodObjArgs(image, "convert", NULL);
if (!image) {
Py_DECREF(pygame);
Py_DECREF(os);
return NULL;
}
我是不是漏掉了什么?有没有更好的方法来处理这个?还有一个问题就是,我可能会忘记所有需要用 Py_DECREF()
来减少引用计数的地方。
5 个回答
这是C语言的接口。如果你只用C语言编程,那你可能得接受这个接口的样子。但如果你的应用是用C++编写的,你可能会想看看一个C++和Python的封装。
这里有两种我可能会写的代码方式,受到我在两种大量使用宏的伪汇编语言中的经验影响,其中一种不是C语言。我把对fullname的解引用移到了这里,不是因为你代码中的写法错了,而是我想展示在这两种方案中如何处理一个使用时间较长的资源。所以想象一下,“fullname”在后面的代码中还会用到:
箭头代码
result = NULL;
py_fullname = PyObject_CallMethod(os, "path.join", "ss", folder, filename);
if (py_fullname) {
image = PyObject_CallMethodObjArgs(pygame, "image.load", py_fullname, NULL);
if (image) {
image = PyObject_CallMethodObjArgs(image, "convert", NULL);
result = // something to do with image, presumably.
}
Py_DECREF(py_fullname);
}
Py_DECREF(pygame);
Py_DECREF(os);
return result;
这个游戏的规则是,每当你调用一个返回资源的函数时,你要立刻检查返回值(或者在释放一些不再需要的资源后再检查,就像你代码中的例子那样),而成功调用对应的代码块必须在退出之前,要么释放这个资源,要么把它赋值给一个返回值,或者直接返回它。通常这会发生在代码块的第二行,第一次使用后,或者在代码块的最后一行。
之所以叫“箭头代码”,是因为如果你在一个函数中调用了5到6次这样的函数,你的代码就会缩进5到6层,看起来像一个“右转”标志。当这种情况发生时,你要么重构代码,要么违背你所有的Python习惯,使用制表符进行缩进,减少制表符的停靠位置;-)
跳转代码
result = NULL;
py_fullname = PyObject_CallMethod(os, "path.join", "ss", folder, filename);
if (!py_fullname) goto cleanup_pygame
image = PyObject_CallMethodObjArgs(pygame, "image.load", py_fullname, NULL);
if (!image) goto cleanup_fullname
image = PyObject_CallMethodObjArgs(image, "convert", NULL);
result = // something to do with image, presumably.
cleanup_fullname:
Py_DECREF(py_fullname);
cleanup_pygame:
Py_DECREF(pygame);
Py_DECREF(os);
return result;
这个跳转代码在结构上和箭头代码是一样的,只是缩进少一些,更容易出错,可能会跳到错误的标签。在某些情况下,你在成功时清理的资源和失败时清理的资源是不同的(比如说,如果你正在构造并返回某个东西,那么在失败时你需要清理到目前为止做的所有事情,而在成功时你只需要清理那些不返回的部分)。在这些情况下,跳转代码明显优于箭头代码,因为你可以为这两种情况设置不同的清理路径,但它们看起来仍然是一样的,出现在例程的最后,甚至可能共享代码。所以你可能会得到这样的代码:
result = NULL;
helper = allocate_something;
if (!helper) goto return_result;
result = allocate_something_else;
if (!result) goto error_return; // OK, result is already NULL, but it makes the point
result->contents = allocate_another_thing;
if (!result->contents) goto error_cleanup_result;
result->othercontents = allocate_last_thing;
if (!result->othercontents) goto error_cleanup_contents;
free_helper:
free(helper);
return_result:
return result;
error_cleanup_contents:
free(result->contents);
error_cleanup_result:
free(result);
error_return;
result = NULL;
goto free_helper;
是的,这样的代码很糟糕,Python或C++程序员看到它可能会感到恶心。如果我再也不需要写这样的代码,我也不会太失望。但只要你有一个系统化的资源清理方案,你应该总是清楚在出错时应该跳到哪个错误标签,而这个错误标签应该“知道”清理到目前为止分配的所有资源。按照相反的顺序进行清理可以让代码共享。而一旦你习惯了这样做,实际上有两件事情是相对简单的:第一,从任何给定的错误标签跟踪路径到退出,确认所有应该释放的资源都被释放了。第二,看到两个错误情况之间的区别,并确认这就是所需的错误处理之间的正确区别,因为这个区别正是释放在跳转到那些标签之间分配的东西。
话虽如此,一个半 decent 的优化编译器会把你例子中的错误情况的代码合并起来。只是当你有这样的代码复制粘贴在各处时,更容易出错,尤其是在你稍后修改它时。
这就是为什么在C语言中,goto
这个东西还存在(虽然它并不是特别受欢迎;-)),而在C++和其他支持异常处理的语言中就不太常见。因为在C语言中,goto
是避免在代码中到处重复写结束处理代码的一个不错的办法。你可以在每次检查返回值时,使用一个条件跳转到 errorexit
,这个地方有一个标签 errorexit:
,专门处理一些清理工作,比如减少引用计数、关闭文件,或者你在结束时需要做的其他事情,然后再执行 return NULL
。