【问题标题】:How important is it to check return values when using the Python C API?使用 Python C API 时检查返回值有多重要?
【发布时间】:2009-12-10 22:55:33
【问题描述】:

似乎每次调用返回 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() 的所有内容。

【问题讨论】:

  • 你为什么不使用 Cython + C ?我遇到的唯一必须使用 Python C API 的情况是在从 Cython 调用的一些线程 C 代码中,有时必须调用纯 Python 代码(它代表约 10 行 Python C API)......在所有其他情况下,我已经能够用 Cython 完全包装纯 C 代码,而不必担心 C API。
  • 这很有趣,我不知道。尽管如此,这更像是练习一点 C 并让它做一些有用的事情的借口。

标签: python c memory-management


【解决方案1】:

这就是为什么goto 在 C 编码(与 C++ 和其他支持异常的语言相反)中仍然存在的原因(尽管不完全好;-):这是唯一体面的方法,不会在整个过程中出现这种重复的终止块代码的主线 - 在每次返回值检查时有条件地向前跳转到 errorexit,带有标签 errorexit: 执行 decrefs(并且文件关闭,以及您在终止时需要执行的任何其他操作)和 @987654324 @。

【讨论】:

  • #define EXCIFZERO(x) if (!x) goto errorexit这样的“化妆品”可以提供帮助
【解决方案2】:

以下是我编写代码的两种方式,受我使用两种高度宏化的伪汇编语言编写经验的影响,其中一种不是 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 级缩进,并且您的函数看起来像一个“右转”标志。当这种情况发生时,你要么重构,要么违背你的每一个 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;

这个 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++ 程序员看到它会身体不适。如果我再也不用写这样的代码,我就不会那么失望了。但是只要你有一个如何清理资源的系统方案,你应该总是知道当出现问题时要跳转到哪个错误标签,并且那个错误标签应该“知道”清理所有已分配的资源,所以远的。以相反的顺序执行它允许通过共享代码。一旦你习惯了它,做两件事就相当容易了:首先沿着从任何给定错误标签到出口的路径,并确认所有应该释放的东西都被释放了。其次,查看两个错误案例之间的差异,并确认这是所需的错误处理之间的正确差异,因为差异正是将在跳转之间分配的东西释放到那些标签。

也就是说,一个半体面的优化编译器将为您的示例中的错误情况提供代码。当你在这样的地方复制和粘贴代码时,更容易出错,尤其是当你以后修改它时。

【讨论】:

  • 有趣的是,这根本没有被投票。这是一篇很好的文章,介绍了在 C 中通常如何在返回前进行​​清理。
【解决方案3】:

这是 C API。如果您只用 C 编写代码,您可能不得不忍受它,但如果您的应用程序是用 C++ 编写的,您可能需要查看 C++/Python wrapper

【讨论】:

    【解决方案4】:

    这也是 C++ 引入异常处理和 RAII 的原因之一。假设您可以使用 C++,您可以创建一个调用 C 函数、测试结果并在发生错误时引发异常的函数。这样,您可以在不进行任何检查的情况下调用包装器......如果发生错误,它将抛出异常。但是,无需重新发明轮子,请查看 Boost.Python 库。

    【讨论】:

      【解决方案5】:

      虽然我不经常看到它,但我认为这对 C 程序员来说是一个很好的解决方案(“the 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;
      

      有几个优点:

      1. 函数中只有一个出口 - 您可以在最后设置一个断点并确保它有效
      2. 清理代码不重复
      3. 没有级联 ifs
      4. 尽早完成错误检查并准确指出问题所在(跟踪消息)
      5. 初始化所有变量无论如何都是个好主意,那么为什么不在清理部分使用它呢?

      我建议写一些宏来统一代码:

      #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)
      

      【讨论】:

        猜你喜欢
        • 2017-08-13
        • 1970-01-01
        • 1970-01-01
        • 2013-10-04
        • 2021-01-13
        • 1970-01-01
        • 1970-01-01
        • 2010-09-30
        • 2016-01-18
        相关资源
        最近更新 更多