似乎每次我调用一个返回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()
的所有东西。
答案 0 :(得分:10)
这就是为什么goto
在C编码中存在(尽管不完全;-)的原因(与C ++和支持异常的其他语言相反):它是唯一不具备重复终止块的方法在代码的主线上 - 在每次返回值检查时有条件地向前跳转到errorexit
,带有执行递减的标签errorexit:
(文件关闭,以及您需要做的其他任何事情)终止)和return NULL
。
答案 1 :(得分:3)
以下是我编写该代码的两种方法,受我使用两种重要的宏编译伪汇编语言编写的经验的影响,其中一种不是C.我已经移动了全名的deref,而不是因为它错了在您的代码中,但是因为我想演示如何在两个方案中处理更长寿的资源。因此,想象一下,例程中将再次需要“全名”:
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级缩进,而你的函数看起来像是一个“向右转”的符号。当发生这种情况时,你要么重构,要么你反对你的每个Pythonic本能,使用标签进行缩进,并减少制表位; - )
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;
此goto代码在结构上与箭头代码完全相同,只是更少缩进,更容易搞乱并跳转到错误的标签。在某些情况下,你将从失败后清理的东西中清除不同的资源(例如,如果你正在构建和返回一些东西,那么在失败时你需要清理你到目前为止所做的一切,但是在成功的时候你只清理你没有回来的东西)。这些情况下goto代码明显胜过箭头代码,因为你可以为这两种情况分别设置清理路径,但它们看起来仍然相同,在例程结束时一起显示,甚至可能共享代码。所以你最终会得到这样的东西:
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 ++程序员在看到它时会身体不适。如果我再也不必写这样的代码了,我就不会那么失望了。但只要你有一个关于如何清理资源的系统方案,你应该总是确切地知道在出现问题时要跳转到哪个错误标签,并且错误标签应该“知道”清理所有已经分配的资源。远。以相反的顺序执行此操作允许直通共享代码。一旦你习惯了它,做两件事就相当容易:首先遵循从任何给定错误标签到退出的路径,并确认释放应该释放的所有内容。其次,查看两个错误情况之间的差异,并确认这是所需错误处理之间的正确差异,因为差异正是为了释放跳转到那些之间的东西。标签
也就是说,一个半合适的优化编译器会为你的例子中的错误情况共同编写代码。当您对这样的地方进行代码复制和粘贴时,更容易犯错,尤其是在您稍后修改它时。
答案 2 :(得分:2)
这是C API。如果您只使用C编写代码,则可能需要使用它,但如果您的应用程序是用C ++编写的,那么您可能需要查看C++/Python wrapper。
答案 3 :(得分:0)
这是C ++引入异常处理和RAII的原因之一。假设您可以使用C ++,您可以创建一个调用C函数的函数,测试结果,并在发生错误时抛出异常。这样,您可以在不进行任何检查的情况下调用包装器...如果发生错误,它将抛出异常。但是,没有必要重新发明轮子,请查看Boost.Python库。
答案 4 :(得分:0)
虽然我不经常看到它,但我认为这对C程序员来说是一个很好的解决方案(“goto with tie”):
result = NULL;
// make sure all variables are initialized
do
{
py_fullname = PyObject_CallMethod(os, "path.join", "ss", folder, filename);
if (!py_fullname)
{
// some additional error handling here
// write a trace message with __FILE__ and __LINE__
break;
}
image = PyObject_CallMethodObjArgs(pygame, "image.load", py_fullname, NULL);
if (!image)
{
// some additional error handling here
break;
}
image = PyObject_CallMethodObjArgs(image, "convert", NULL);
result = // something to do with image, presumably.
} while (true);
if (py_fullname)
Py_DECREF(py_fullname);
if (pygame)
Py_DECREF(pygame);
if (os)
Py_DECREF(os);
return result;
有几个优点:
我建议编写一些宏来统一代码:
#define CATCH_BEGIN do {
#define CATCH_END } while (1!=1);
#define CLEANUP_VOID(function,var) {if (var != NULL) { function(var); var = NULL;}}
这允许最后清理:
CLEANUP_VOID(Py_DECREF, py_fullname)
CLEANUP_VOID(Py_DECREF, pygame)
CLEANUP_VOID(Py_DECREF, os)