修改或重新引发 C API 中的 Python 错误

Modifying or Reraising Python error in C API

我有一些代码试图将对象解析为整数:

long val = PyLong_AsLong(obj);
if(val == -1 && PyErr_Occurred()) {
    return -1;
}

这里 obj 是普通的 PyObject *,如果 obj 不是整数,PyLong_AsLong 会引发一个非常通用的 TypeError

我想将错误消息转换成更具信息性的内容,因此我想修改现有的错误对象,或者重新提出它。

我目前的解决方案是:

long val = PyLong_AsLong(obj);
if(val == -1 && PyErr_Occurred()) {
    PyErr_Clear();
    PyErr_Format(PyExc_TypeError, "Parameter must be an integer type, but got %s", Py_TYPE(obj)->tp_name);
    return -1;
}

这是重新引发错误的正确方法吗?具体来说,

  1. 我需要打电话给 PyErr_Clear 吗?我怀疑它正确地减少了现有的异常对象,但我不确定。
  2. 我可以修改当时已经抛出的错误消息而不重新引发它吗?
  3. 是否有一个选项可以执行与 raise new_err from old_err 相同的操作?

我不确定如何在这种情况下使用 PyErr_SetExcInfo,尽管我的直觉告诉我它可能以某种方式相关。

您现有的代码很好,但如果您想执行异常链接的等效操作,则可以。如果您想跳至如何操作,请跳至答案结尾附近的第 3 点。


要解释如何执行修改传播异常或执行等同于 raise Something() from existing_exception 的操作,首先,我们必须解释异常状态在 C 级别是如何工作的。

传播异常由 per-thread error indicator consisting of a type, value, and traceback. That sounds a lot like sys.exc_info() 表示,但它并不相同。 sys.exc_info() 用于 被 Python 级代码捕获 的异常,而不是仍在传播的异常。

错误指示符可能是未规范化,这基本上意味着构造异常对象的工作还没有执行, 错误指示器中不是异常 type 的实例。这种状态是为了效率而存在的;如果在需要规范化之前错误指示器被 PyErr_Clear 清除,Python 将跳过引发异常的大部分工作。异常规范化由 PyErr_NormalizeException, with a bit of extra work in PyException_SetTraceback 执行以设置异常对象的 __traceback__ 属性。

PyErr_Clear 有点像 except 块的 C 等价物,但它只是清除错误指示器,而不会让您检查很多异常信息。要捕获异常并检查它,您需要 PyErr_FetchPyErr_Fetch 就像捕获异常并检查 sys.exc_info(),但它不会设置 sys.exc_info() 或规范化异常。它清除错误指示器并直接为您提供错误指示器的原始内容。

显式异常链接 (raise Something() from existing_exception) 通过 PyException_SetCause 将新异常的 __cause__ 设置为现有异常。这两个异常都需要异常对象,所以如果你想从 C 中做同样的事情,你必须规范化异常并自己调用 PyException_SetCause

隐式异常链接(raise Something()except 块中)通过 PyException_SetContext 将新异常的 __context__ 设置为现有异常。类似于PyException_SetCause,这需要异常对象和异常规范化。 except 块中的 raise Something() from existing_exception 实际上设置了 __cause____context__,如果你想在 C 级别执行显式异常链接,你通常应该这样做。


  1. 据我所知,技术上没有必要,但无论如何这样做可能是个好主意。看起来 PyErr_Format 和其他设置错误指示器的函数将首先清除错误指示器(如果已经设置),但大多数函数都没有记录。
  2. 有点,但这可能是个坏主意。您可以规范化错误指示器并设置异常对象的 message 属性,但这不会影响 args 或异常 class 可能对其参数所做的任何其他事情,这可能会导致奇怪的问题。或者,您可以使用 PyErr_Fetch 获取错误指示器并使用 PyErr_Restore 的值的新字符串恢复它,但是如果有一个现有的异常对象,它会丢弃它,并且它会做出关于异常 class 的签名。
  3. 是的,这是可能的,但是通过 public C API 函数来完成它是相当笨拙和手动的。您必须手动执行大量规范化、取消引发和引发异常。

    There are efforts to make C-level exception chaining more convenient, but so far, the more convenient functions are all considered internal. For example, _PyErr_FormatFromCause 类似于 PyErr_Format,但它将新异常链接到现有的传播异常(通过 __context____cause__.

    我暂时不建议直接调用它;它是非常新的(3.6+),而且很可能会发生变化(具体来说,如果看到它在新的 Python 版本中失去其前导下划线,我不会感到惊讶)。相反,复制 implementation of _PyErr_FormatFromCause/_PyErr_FormatVFromCause (and respecting the license) 是确保您拥有正确的规范化和链接的繁琐位的好方法。

    如果您想在 C 级别执行隐式(仅 __context__)异常链接,它也是一个有用的工作参考 - 只需删除处理 __cause__.[=56= 的部分]