问题描述
如果可能设置了错误指示符,则C API for Python中的许多函数都不安全。
特别是, PyFloat_AsDouble
和类似的函数是不明确的 ,因为它们没有保留用于指示错误的返回值:如果它们成功(但恰好返回用于错误的值),调用PyErr_Occurred
的客户端将认为它们失败如果错误指示器已经设置好了。
(注意,这或多或少地保证在PyIter_Next
发生。)更一般地说,任何可以失败的函数都会覆盖错误指示符(如果有的话),这可能是也可能不是。
不幸的是,使用错误指示符集调用此类函数的可能性根本不太可能:对错误的常见反应是Py_DECREF
局部变量,并且(除非可以(间接)释放它的所有对象的类型是已知的)可以执行任意代码。
(这是清除代码可能失败的危险的一个很好的例子。)解释器捕获在这种析构函数中引发的异常,但它不会阻止异常泄漏到它们中 。
在任何一端 ,我们都可以使用PyErr_Fetch
和PyErr_Restore
来防止这些问题。
通过调用模糊函数,它们可以可靠地确定它是否成功;
放在Py_DECREF
周围,??它们可以防止在执行任何易受影响的代码时设置错误指示器。
(它们甚至可以在可能失败的直接调用的清理代码周围使用,以便允许选择传播哪个异常。毫无疑问在这种情况下将它放在何处:清理代码无法在多个异常之间进行选择。)
放置选择会显着增加代码复杂性和执行时间:对模糊函数的调用很多,错误处理路径上有很多Py_DECREF
。
虽然防御性编程的原则会建议在两个地方使用它,但是(通过仔细编程) 通用约定(覆盖正在执行的任意代码)会产生更好的代码。
C本身有这样一个约定: errno
必须由任意代码的调用者保存,即使(如Python析构函数中的抑制异常)代码不希望将errno
为任何东西。
主要原因是它可以被许多成功的库调用重置(但永远不会为0)(让它们在内部处理错误),进一步缩小了安全执行的操作集,而errno
具有一些重要的值。
(这也可以防止当PyErr_Occurred
报告预先存在的错误时出现的问题:C程序员必须在调用不明确的函数之前将errno
为0.)另一个原因是“调用一些没有错误报告的任意代码”不是常见的操作大多数C程序,因此加重其他代码的负担将是荒谬的。
是否有这样的约定(即使有错误的代码在CPython本身不遵循它)? 如果做不到,是否有技术上的理由指导一个人的选择? 或者这可能是一个工程问题,基于太字面的“任意”读取:CPython是否应该在处理析构函数异常时保存并恢复错误指示器本身?
1楼
如果你的清理只是一堆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_Fetch
和PyErr_Restore
异常状态时,即可大功告成。
如果在清理过程中出现了另一个异常,你可以链接它( 在C级别),或者用向stderr 一个简短的警告,然后通过PyErr_Clear
抑制新的异常 - 或者通过PyErr_Restore
-原始的异常状态。