如果 PyModule_Add* 函数失败,C 扩展是否应该在模块初始化中失败?

Should a C extension fail in module init if a PyModule_Add* function fails?

我刚刚审查了一些代码,这些代码为 Python 创建了一个 C 扩展模块,其中没有包含足够的错误检查。在大多数情况下这很容易,但是当涉及到 module-init 函数时我不确定。

为了便于讨论,我们拿(删节的)module-init function for itertools(是的,CPython发的那个)来说:

m = PyModule_Create(&itertoolsmodule);
if (m == NULL)
    return NULL;

for (i=0 ; typelist[i] != NULL ; i++) {
    if (PyType_Ready(typelist[i]) < 0)
        return NULL;
    name = strchr(typelist[i]->tp_name, '.');
    assert (name != NULL);
    Py_INCREF(typelist[i]);
    PyModule_AddObject(m, name+1, (PyObject *)typelist[i]);
}

return m;

它会检查 PyModule_Create 是否失败(这很好),然后它会检查 PyType_Ready 是否失败(这很好),但在那种情况下它不会 Py_DECREF(m)(这是 suprising/confusing) 但它完全无法检查 PyModule_AddObject 是否失败。根据 it's documentation 可以 失败:

Add an object to module as name. This is a convenience function which can be used from the module’s initialization function. This steals a reference to value. Return -1 on error, 0 on success.

好的,以防万一无法添加类型,破坏模块初始化似乎有些矫枉过正。但即使他们不想完全中止创建模块:它应该泄漏 typelist[i] 的引用,对吗?

许多内置 CPython C 模块在模块初始化函数中没有进行彻底的错误检查和处理(这可能是我正在修复的 C 扩展也没有它们的原因) 而且他们通常对这类问题和潜在的泄漏非常严格。所以我的问题基本上是:模块初始化函数中的错误检查是否重要,尤其是涉及 PyModule_Add* 函数(如 PyModule_AddObject)时?或者它们可以像 CPython 在许多地方所做的那样被省略吗?

我通常赞成在使用 Python 的 C 时进行严格的错误检查 API - 人们经常编写冗长的多步函数,不检查任何错误,然后当它神秘地失败时表现出困惑。在这种情况下(模块初始化),您可以通过错误检查来证明稍微松懈:

主要原因是这些函数只会因为您的 C 代码中的错误而真正失败,并且它们会重复执行此操作 - 它们几乎不可能在毫无戒心的用户身上意外地失败。以PyModule_AddObject为例,会失败是因为:

  • 传递的第一个参数不是模块(你的错误!)
  • 传递的对象是 NULL(您应该早点检查)
  • 该模块没有 __dict__(我不知道这是怎么发生的,但我看不到它是偶然发生在您刚创建的模块上的)
  • PyDict_SetItemString 失败(很可能是由于 PyUnicode_FromString 失败)。

正如您在评论中指出的那样,后者可能是由 MemoryError 引起的(这可能随时发生并且不可预测)。但是,当您从分配 ~10 个字符串中获得 MemoryErrors 时,Python 解释器不太可能继续运行更长时间。

所以我想我的结论是"if your module seems to be working you probably don't need this error checking, but if things are going wrong then it's useful for finding out where"。我可能要添加的一件事是在 return 模块之前对错误进行最终检查:

if (PyErr_Occurred()) return NULL;
/* or */
if (PyErr_Occurred()) {
    /* print a warning? */
    PyErr_Clear();
    return m;
}

原因是 Python 如果设置了错误指示器但你没有设置 return NULL 可能会表现得很奇怪(你会在 odd没有意义的时间)。因此,快速的最终检查具有一定的价值。


关于模块初始化失败时的引用处理:显然 "best" 做对了,但我认为您可以跳过它。这是运行一次的代码(因此您不会因反复丢失少量内存而丢失大量内存)。如果发生错误,那么最有可能的选择是程序中止(因此所有内存都被恢复)。即使您不中止,泄漏的大小也可能非常小(实际上约为 100 字节)。