使用Python C API时检查返回值有多重要?

使用Python C API时检查返回值有多重要?,python,c,memory-management,Python,C,Memory Management,似乎每次调用返回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

似乎每次调用返回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()

这是C API。如果只在C中编码,则可能需要使用它,但是如果应用程序是用C++编码的,那么您可能需要查看A.< /P>< P>这就是为什么代码> Goto < /C> >在C代码中是活的(尽管不是完全好的)——(与C++和支持异常的其他语言相反):这是唯一一种不在代码主线上重复终止块的好方法——在每次返回值检查时,有条件地向前跳转到
errorexit
,带有标签
errorexit:
,该标签执行递减操作(文件关闭,以及终止时需要执行的任何操作)而返回null .< /P> < P>这是C++引入异常处理和RAII的原因之一。假设您可以使用C++,可以创建一个函数,调用C函数,测试结果,如果发生错误,抛出异常。这样,您就可以调用包装器而无需任何检查。。。如果发生错误,它将抛出异常。但是,没有必要重新发明轮子,请查看库。

受我用两种高度宏化的伪汇编语言(其中一种不是C)编写代码的经验影响,我有两种编写代码的方法。我移动了fullname的顺序,不是因为您的代码中有错误,但因为我想演示如何在这两种方案中处理寿命更长的资源。因此,想象一下,在例程的后面部分将再次需要“全名”:

箭头代码 这个游戏的玩法是,无论何时调用返回资源的函数,您都会立即检查返回值(或者在释放一些不再需要的资源后,如示例代码中所示),与成功调用对应的块必须释放资源,或者将其分配给返回值,或者在块退出之前实际返回它。这通常在第一行中使用后,在块的第二行中,或者在块的最后一行中

它被称为“箭头代码”,因为如果你在一个函数中进行5或6次这样的调用,你最终会得到5或6级缩进,你的函数看起来就像一个“向右拐”的标志。当这种情况发生时,您要么重构,要么违背您的每一个Python本能,使用制表符进行缩进,并减少制表符停止;-)

后藤 这种goto代码在结构上与arrow代码相同,只是缩进更少,更容易弄乱并跳转到错误的标签。在某些情况下,你会在成功时清理不同于在失败时清理的资源(例如,如果你正在构建并返回某个东西,那么在失败时,你需要清理你迄今为止所做的任何事情,但在成功时,你只清理你没有返回的东西)。在这种情况下,goto代码明显优于arrow代码,因为对于这两种情况,您可以有单独的清理路径,但它们看起来仍然相同,在例程结束时一起出现,甚至可能共享代码。所以你可能会得到这样的结果:

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;
CLEANUP_VOID(Py_DECREF, py_fullname)
CLEANUP_VOID(Py_DECREF, pygame)
CLEANUP_VOID(Py_DECREF, os)
是的,这很可怕,Python或C++程序员在看到它时身体会很不舒服。如果我再也不用写这样的代码了,我就不会那么失望了。但是,只要您有一个关于如何清理资源的系统方案,您就应该始终准确地知道在出现错误时跳转到哪个错误标签,并且该错误标签应该“知道”以清理迄今为止分配的所有资源。按相反的顺序操作可以让fall-through共享代码。一旦你习惯了,做两件事就相当容易了:首先,沿着从任何给定的错误标签到出口的路径,确认所有应该释放的都被释放了。其次,查看两种错误情况之间的差异,并确认这是所需错误处理之间的正确差异,因为这种差异正是为了将在跳转之间分配的内容释放到这些标签


这就是说,一个半体面的优化编译器将为您的示例中的错误情况共享代码。当你有代码拷贝并粘贴在这样的地方时,更容易出错,尤其是当你稍后修改它的时候。

虽然我不经常看到它,但我认为这是C程序员(“Toto TooType”)的一个很好的解决方案:

有几个优点:

  • 函数中只有一个出口-您可以在末尾设置断点并确保其正常工作
  • 清理代码不重复
  • 没有级联ifs
  • 尽早完成错误检查,并准确指出问题所在(跟踪消息)
  • 初始化所有变量是个好主意,那么为什么不在清理部分使用它呢
  • 我建议编写一些宏以统一代码:

    #define CATCH_BEGIN     do {
    #define CATCH_END       } while (1!=1);
    #define CLEANUP_VOID(function,var) {if (var != NULL) { function(var); var = NULL;}}
    
    这样可以在末尾进行如下清理:

    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;
    
    CLEANUP_VOID(Py_DECREF, py_fullname)
    CLEANUP_VOID(Py_DECREF, pygame)
    CLEANUP_VOID(Py_DECREF, os)
    

    “化妆品”如
    #定义EXCIFZERO(x)如果(!x)转到errorexit
    可以帮助您为什么不使用Cython+C?我遇到的唯一一个必须使用Python C API的情况是,在Cython调用的一些线程C代码中,有时不得不调用纯Python代码(它代表了大约10行Python C API)。。。在所有其他情况下,我都能够用Cython完全包装纯C代码,而且从不担心C API。这很有趣,我不知道。尽管如此,这更像是一个借口来练习一点C,让它做一些有用的事情。有趣的是,这根本没有被投票支持。这是一篇很好的文章,介绍了在C语言中,如何在返回前进行清洗。
    CLEANUP_VOID(Py_DECREF, py_fullname)
    CLEANUP_VOID(Py_DECREF, pygame)
    CLEANUP_VOID(Py_DECREF, os)