当前位置: 代码迷 >> python >> 谁应该打电话给PyErr_Fetch?
  详细解决方案

谁应该打电话给PyErr_Fetch?

热度:98   发布时间:2023-06-21 10:58:50.0

如果可能设置了错误指示符,则C API for Python中的许多函数都不安全。 特别是, PyFloat_AsDouble和类似的函数是不明确的 ,因为它们没有保留用于指示错误的返回值:如果它们成功(但恰好返回用于错误的值),调用PyErr_Occurred的客户端将认为它们失败如果错误指示器已经设置好了。 (注意,这或多或少地保证在PyIter_Next发生。)更一般地说,任何可以失败的函数都会覆盖错误指示符(如果有的话),这可能是也可能不是。

不幸的是,使用错误指示符集调用此类函数的可能性根本不太可能:对错误的常见反应是Py_DECREF局部变量,并且(除非可以(间接)释放它的所有对象的类型是已知的)可以执行任意代码。 (这是清除代码可能失败的危险的一个很好的例子。)解释器捕获在这种析构函数中引发的异常,但它不会阻止异常泄漏它们中

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

放置选择会显着增加代码复杂性和执行时间:对模糊函数的调用很多,错误处理路径上有很多Py_DECREF 虽然防御性编程的原则会建议在两个地方使用它,但是(通过仔细编程) 通用约定(覆盖正在执行的任意代码)会产生更好的代码。

C本身有这样一个约定: errno必须由任意代码的调用者保存,即使(如Python析构函数中的抑制异常)代码不希望将errno为任何东西。 主要原因是它可以被许多成功的库调用重置(但永远不会为0)(让它们在内部处理错误),进一步缩小了安全执行的操作集,而errno具有一些重要的值。 (这也可以防止当PyErr_Occurred报告预先存在的错误时出现的问题:C程序员必须在调用不明确的函数之前将errno为0.)另一个原因是“调用一些没有错误报告的任意代码”不是常见的操作大多数C程序,因此加重其他代码的负担将是荒谬的。

是否有这样的约定(即使有错误的代码在CPython本身不遵循它)? 如果做不到,是否有技术上的理由指导一个人的选择? 或者这可能是一个工程问题,基于太字面的“任意”读取:CPython是否应该在处理析构函数异常时保存并恢复错误指示器本身?

如果你的清理只是一堆Py_DECREF ,你不需要调用PyErr_Fetch Py_DECREF旨在安全地使用异常集进行调用。 如果Py_DECREF的代码需要做一些与异常集不安全的事情,它将负责保存和恢复异常状态。 (如果您的清理不仅仅涉及Py_DECREF ,您可能需要自己处理。)

例如, tp_finalize是最有可能调用任意Python代码的对象破坏步骤之一, :

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__方法,您可以在看到相关的处理:

/* 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);

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

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级别),或者用向stderr 一个简短的警告,然后通过PyErr_Clear抑制新的异常 - 或者通过PyErr_Restore -原始的异常状态。

  相关解决方案