谁应该打电话给PyErr_Fetch?
Who should call PyErr_Fetch?
C API 中用于 Python 的许多函数在可能设置错误指示符的情况下使用不安全。特别是,PyFloat_AsDouble
和类似的函数 不明确 因为它们没有 return 值保留用于指示错误:如果它们成功(但碰巧 return 用于错误的值),如果错误指示器已经设置,则调用 PyErr_Occurred
的客户端将认为它们失败了。 (请注意,PyIter_Next
或多或少会保证发生这种情况。)更一般地说,任何可能失败的函数都会覆盖错误指示器,如果失败的话,这可能是可取的,也可能不是可取的。
不幸的是,使用错误指示符集调用此类函数的可能性一点也不小:对错误的常见反应是 Py_DECREF
局部变量,并且(除非所有对象的类型可能被(间接)释放是已知的)可以执行任意代码。 (这是一个很好的例子,说明清理代码可能会失败。)解释器在此类析构函数 中捕获异常 raised,但它不会阻止异常从泄漏 到 他们.
在任一端,我们可以使用PyErr_Fetch
和PyErr_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代码的对象销毁步骤之一,is explicitly responsible for saving and restoring an active exception:
tp_finalize
should not mutate the current exception status;
therefore, a recommended way to write a non-trivial finalizer is:
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_Fetch
和 PyErr_Restore
异常状态。如果在清理时出现另一个异常,您可以将它链接起来( at C level), or dump a short warning to stderr with PyErr_WriteUnraisable
然后通过 PyErr_Clear
-ing 或 PyErr_Restore
-ing 原始异常来抑制新异常陈述它。
C API 中用于 Python 的许多函数在可能设置错误指示符的情况下使用不安全。特别是,PyFloat_AsDouble
和类似的函数 不明确 因为它们没有 return 值保留用于指示错误:如果它们成功(但碰巧 return 用于错误的值),如果错误指示器已经设置,则调用 PyErr_Occurred
的客户端将认为它们失败了。 (请注意,PyIter_Next
或多或少会保证发生这种情况。)更一般地说,任何可能失败的函数都会覆盖错误指示器,如果失败的话,这可能是可取的,也可能不是可取的。
不幸的是,使用错误指示符集调用此类函数的可能性一点也不小:对错误的常见反应是 Py_DECREF
局部变量,并且(除非所有对象的类型可能被(间接)释放是已知的)可以执行任意代码。 (这是一个很好的例子,说明清理代码可能会失败。)解释器在此类析构函数 中捕获异常 raised,但它不会阻止异常从泄漏 到 他们.
在任一端,我们可以使用PyErr_Fetch
和PyErr_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代码的对象销毁步骤之一,is explicitly responsible for saving and restoring an active exception:
tp_finalize
should not mutate the current exception status; therefore, a recommended way to write a non-trivial finalizer is: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_Fetch
和 PyErr_Restore
异常状态。如果在清理时出现另一个异常,您可以将它链接起来(PyErr_WriteUnraisable
然后通过 PyErr_Clear
-ing 或 PyErr_Restore
-ing 原始异常来抑制新异常陈述它。