谁应该致电PyErr_Fetch?

时间:2018-09-23 17:07:28

标签: python error-handling cpython python-internals

如果可能设置了错误指示符,则C API for Python中的许多函数都不安全使用。特别是PyFloat_AsDouble和类似的功能含糊的,因为它们没有保留用于指示错误的返回值:如果它们成功(但碰巧返回用于错误的值),则客户端如果仅设置了错误指示符,则调用PyErr_Occurred的调用将认为它们已失败。 (请注意,PyIter_Next或多或少会发生这种情况。)更一般而言,任何可能失败的函数都会覆盖错误指示符(如果有的话),这可能是不希望的。

不幸的是,使用错误指示符集调用此类函数的可能性一点也不不可能:对错误的常见反应是对Py_DECREF局部变量,和(除非所有对象的类型可能是(间接地)释放,可以执行任意代码。 (这是清除代码可能导致失败的危险的一个很好的例子。)解释器在此类析构函数中捕获了 raised 引发的异常,但并不能防止异常泄漏进入

两端,我们可以使用PyErr_FetchPyErr_Restore来防止这些问题。围绕一个模糊函数的调用,它们可以可靠地确定它是否成功;放在Py_DECREF周围,它们可以防止在执行任何敏感代码时首先设置错误指示符。 (它们甚至还可以在可能会失败的直接调用的清理代码周围使用,以便允许选择传播哪个异常。在这种情况下,将其放置在哪里毫无疑问:清理代码无论如何都无法在多个异常之间进行选择)

任何一种放置方式都显着增加了代码复杂度和执行时间:对歧义函数的调用很多,并且在错误处理路径上有很多Py_DECREF。虽然防御性编程的原理建议在两个地方都使用它,但通用约定(仔细编程)会产生更好的代码(以覆盖正在执行的任意代码) )。

C本身具有这样的约定:errno必须由任意代码的调用者保存,即使(例如Python析构函数中受抑制的异常)该代码不应期望将errno设置为任何值。主要原因是可以通过许多成功库调用(使其不能在内部处理错误)将其重置(但永远不能重置为0),从而进一步缩小了{{1} }具有一些重要的价值。 (这也防止了errno报告先前存在的错误时出现的问题:C程序员必须在调用歧义函数之前将PyErr_Occurred设置为0。)另一个原因是“无误地调用任意代码报告”在大多数C程序中都不是常见的操作,因此为此负担其他代码将是荒谬的。

是否存在这样的约定(即使CPython本身中没有遵循它的错误代码)?失败的话,是否有技术上的理由来指导一个人的选择?也许这是一个工程上的问题,它是基于过于随意的“任意”阅读:CPython是否应该在处理析构函数异常时保存并恢复错误指示符本身?

1 个答案:

答案 0 :(得分:3)

如果您的清理工作只是一堆Py_DECREF,则无需致电PyErr_FetchPy_DECREF旨在安全地调用带有异常集的电话。如果Py_DECREF中的代码需要执行对异常集不安全的操作,它将负责保存和恢复异常状态。 (如果清理不仅仅涉及Py_DECREF,您可能需要自己处理。)

例如,tp_finalize是对象破坏步骤之一,最有可能调用任意Python代码is explicitly responsible for saving and restoring an active exception

  

tp_finalize不应更改当前的异常状态;   因此,推荐的写平凡终结器的方法是:

static void
local_finalize(PyObject *self)
{
    PyObject *error_type, *error_value, *error_traceback;

    /* Save the current exception, if any. */
    PyErr_Fetch(&error_type, &error_value, &error_traceback);

    /* ... */

    /* Restore the saved exception. */
    PyErr_Restore(error_type, error_value, error_traceback);
}

对于用Python编写的__del__方法,您可以在slot_tp_finalize中查看相关处理:

/* Save the current exception, if any. */
PyErr_Fetch(&error_type, &error_value, &error_traceback);

/* Execute __del__ method, if any. */
del = lookup_maybe_method(self, &PyId___del__, &unbound);
if (del != NULL) {
    res = call_unbound_noarg(unbound, del, self);
    if (res == NULL)
        PyErr_WriteUnraisable(del);
    else
        Py_DECREF(res);
    Py_DECREF(del);
}

/* Restore the saved exception. */
PyErr_Restore(error_type, error_value, error_traceback);

弱引用系统还takes responsibility用于在调用弱引用回调之前保存异常状态:

if (*list != NULL) {
    PyWeakReference *current = *list;
    Py_ssize_t count = _PyWeakref_GetWeakrefCount(current);
    PyObject *err_type, *err_value, *err_tb;

    PyErr_Fetch(&err_type, &err_value, &err_tb);
    if (count == 1) {
        PyObject *callback = current->wr_callback;

        current->wr_callback = NULL;
        clear_weakref(current);
        if (callback != NULL) {
            if (((PyObject *)current)->ob_refcnt > 0)
                handle_callback(current, callback);
            Py_DECREF(callback);
        }
    }
    else {
        ...

因此,在设置了异常的情况下调用Py_DECREF是令人恐惧的,考虑到这样做是件好事,但是只要对象销毁代码的行为正常,就可以了。


那么,除了清除引用之外,您还需要做更多的清理工作吗?在这种情况下,如果清理不安全且无法处理异常集,则可能应在完成后调用PyErr_FetchPyErr_Restore异常状态。如果在清理过程中引发了其他异常,则可以将其链接(在C级上awkward but possible),或者使用PyErr_WriteUnraisable将简短警告转储到stderr,然后通过{{ 1}}-或通过PyErr_Clear-对其上的原始异常状态进行