9. 异常

9.1 C++内置异常到Python异常的转换

当Python通过pybind11调用C++代码时,pybind11将捕获C++异常,并将其翻译为对应的Python异常后抛出。这样Python代码就能够处理它们。

pybind11定义了std::exception及其标准子类,和一些特殊异常到Python异常的翻译。由于它们不是真正的Python异常,所以不能使用Python C API来检查。相反,它们是纯C++异常,当它们到达异常处理器时,pybind11将其翻译为对应的Python异常。

Exception thrown by C++ Translated to Python exception type
std::exception RuntimeError
std::bad_alloc MemoryError
std::domain_error ValueError
std::invalid_argument ValueError
std::length_error ValueError
std::out_of_range IndexError
std::range_error ValueError
std::overflow_error OverflowError
pybind11::stop_iteration StopIteration (used to implement custom iterators)
pybind11::index_error IndexError (used to indicate out of bounds access in __getitem__, __setitem__, etc.)
pybind11::key_error KeyError (used to indicate out of bounds access in __getitem__, __setitem__ in dict-like objects, etc.)
pybind11::value_error ValueError (used to indicate wrong value passed in container.remove(...))
pybind11::type_error TypeError
pybind11::buffer_error BufferError
pybind11::import_error ImportError
pybind11::attribute_error AttributeError
Any other exception RuntimeError

异常翻译不是双向的。即上述异常不会捕获源自Python的异常。Python的异常,需要捕获pybind11::error_already_set

这里有个特殊的异常,当入参不能转化为Python对象时,handle::call()将抛出cast_error异常。

9.2 注册定制异常翻译

如果上述默认异常转换策略不够用,pybind11也提供了注册自定义异常翻译的支持。类似于pybind11 class,异常翻译也可以定义在模块内或global。要注册一个使用C++异常的what()方法将C++到Python的异常转换,可以使用下面的方法:

  1. py::register_exception<CppExp>(module, "PyExp");

这个调用在指定模块创建了一个名称为PyExp的Python异常,并自动将CppExp相关的异常转换为PyExp异常。

相似的函数可以注册模块内的异常翻译:

  1. py::register_local_exception<CppExp>(module, "PyExp");

方法的第三个参数handle可以指定异常的基类:

  1. py::register_exception<CppExp>(module, "PyExp", PyExc_RuntimeError);
  2. py::register_local_exception<CppExp>(module, "PyExp", PyExc_RuntimeError);

这样,PyExp异常可以捕获PyExp和RuntimeError。

Python内置的异常类型可以参考Python文档Standard Exceptions,默认的基类为PyExc_Exception

py::register_exception_translator(translator)py::register_local_exception_translator(translator) 提供了更高级的异常翻译功能,它可以注册任意的异常类型。函数接受一个无状态的回调函数void(std::exception_ptr)

C++异常抛出时,注册的异常翻译类将以注册时相反的顺序匹配,优先匹配模块内翻译类,然后再是全局翻译类。

Inside the translator, std::rethrow_exception should be used within a try block to re-throw the exception. One or more catch clauses to catch the appropriate exceptions should then be used with each clause using PyErr_SetString to set a Python exception or ex(string) to set the python exception to a custom exception type (see below).

To declare a custom Python exception type, declare a py::exception variable and use this in the associated exception translator (note: it is often useful to make this a static declaration when using it inside a lambda expression without requiring capturing).

The following example demonstrates this for a hypothetical exception classes MyCustomException and OtherException: the first is translated to a custom python exception MyCustomError, while the second is translated to a standard python RuntimeError:

  1. static py::exception<MyCustomException> exc(m, "MyCustomError");
  2. py::register_exception_translator([](std::exception_ptr p) {
  3. try {
  4. if (p) std::rethrow_exception(p);
  5. } catch (const MyCustomException &e) {
  6. exc(e.what());
  7. } catch (const OtherException &e) {
  8. PyErr_SetString(PyExc_RuntimeError, e.what());
  9. }
  10. });

Multiple exceptions can be handled by a single translator, as shown in the example above. If the exception is not caught by the current translator, the previously registered one gets a chance.

If none of the registered exception translators is able to handle the exception, it is handled by the default converter as described in the previous section.

9.3 Local vs Global Exception Translators

When a global exception translator is registered, it will be applied across all modules in the reverse order of registration. This can create behavior where the order of module import influences how exceptions are translated.

If module1 has the following translator:

  1. py::register_exception_translator([](std::exception_ptr p) {
  2. try {
  3. if (p) std::rethrow_exception(p);
  4. } catch (const std::invalid_argument &e) {
  5. PyErr_SetString("module1 handled this")
  6. }
  7. }

and module2 has the following similar translator:

  1. py::register_exception_translator([](std::exception_ptr p) {
  2. try {
  3. if (p) std::rethrow_exception(p);
  4. } catch (const std::invalid_argument &e) {
  5. PyErr_SetString("module2 handled this")
  6. }
  7. }

then which translator handles the invalid_argument will be determined by the order that module1 and module2 are imported. Since exception translators are applied in the reverse order of registration, which ever module was imported last will “win” and that translator will be applied.

If there are multiple pybind11 modules that share exception types (either standard built-in or custom) loaded into a single python instance and consistent error handling behavior is needed, then local translators should be used.

Changing the previous example to use register_local_exception_translator would mean that when invalid_argument is thrown in the module2 code, the module2 translator will always handle it, while in module1, the module1 translator will do the same.

9.4 在C++中处理Python异常

当C++调用Python函数时(回调函数或者操作Python对象),若Python有异常抛出,pybind11会将Python异常转化为pybind11::error_already_set类型的异常,它包含了一个C++字符串描述和实际的Python异常。error_already_set用于将Python异常传回Python(或者在C++侧处理)。

Exception raised in Python Thrown as C++ exception type
Any Python Exception pybind11::error_already_set

举个例子:

  1. try {
  2. // open("missing.txt", "r")
  3. auto file = py::module_::import("io").attr("open")("missing.txt", "r");
  4. auto text = file.attr("read")();
  5. file.attr("close")();
  6. } catch (py::error_already_set &e) {
  7. if (e.matches(PyExc_FileNotFoundError)) {
  8. py::print("missing.txt not found");
  9. } else if (e.matches(PyExc_PermissionError)) {
  10. py::print("missing.txt found but not accessible");
  11. } else {
  12. throw;
  13. }
  14. }

该方法并不适用与C++到Python的翻译,Python侧抛出的异常总是被翻译为error_already_set.

  1. try {
  2. py::eval("raise ValueError('The Ring')");
  3. } catch (py::value_error &boromir) {
  4. // Boromir never gets the ring
  5. assert(false);
  6. } catch (py::error_already_set &frodo) {
  7. // Frodo gets the ring
  8. py::print("I will take the ring");
  9. }
  10. try {
  11. // py::value_error is a request for pybind11 to raise a Python exception
  12. throw py::value_error("The ball");
  13. } catch (py::error_already_set &cat) {
  14. // cat won't catch the ball since
  15. // py::value_error is not a Python exception
  16. assert(false);
  17. } catch (py::value_error &dog) {
  18. // dog will catch the ball
  19. py::print("Run Spot run");
  20. throw; // Throw it again (pybind11 will raise ValueError)
  21. }

9.5 处理Python C API的错误

尽可能地使用pybind11 wrappers代替直接调用Python C API。如果确实需要直接使用Python C API,除了需要手动管理引用计数外,还必须遵守pybind11的错误处理协议。

在调用Python C API后,如果Python返回错误,需要调用throw py::error_already_set();语句,让pybind11来处理异常并传递给Python解释器。这包括对错误设置函数的调用,如PyErr_SetString

  1. PyErr_SetString(PyExc_TypeError, "C API type error demo");
  2. throw py::error_already_set();
  3. // But it would be easier to simply...
  4. throw py::type_error("pybind11 wrapper type error");

也可以调用PyErr_Clear来忽略错误。

任何Python错误必须被抛出或清除,否则Python/pybind11将处于无效的状态。

9.6 异常链(raise from)

在Python 3.3中,引入了指示异常是由其他异常引发的机制:

  1. try:
  2. print(1 / 0)
  3. except Exception as exc:
  4. raise RuntimeError("could not divide by zero") from exc

pybind11 2.8版本,你可以使用py::raise_from函数来完成相同的事。它设置当前Python错误指示器,所以要继续传播异常,你应该throw py::error_already_set()(Python 3 only)。

  1. try {
  2. py::eval("print(1 / 0"));
  3. } catch (py::error_already_set &e) {
  4. py::raise_from(e, PyExc_RuntimeError, "could not divide by zero");
  5. throw py::error_already_set();
  6. }

9.7 处理unraiseable异常

如果Python调用的C++析构函数或任何标记为noexcept(true)的函数抛出了异常,该异常不会传播出去。如果它们在调用图中抛出或捕捉不到任何异常,c++运行时将调用std::terminate()立即终止程序。

类似的,在类__del__方法引发的Python异常也不会传播,但被Python作为unraisable错误记录下来。在Python 3.8+中,将触发system hook,并记录auditing event日志。

任何noexcept函数应该使用try-catch代码块来捕获error_already_set(或其他可能出现的异常)。pybind11包装的Python异常并非真正的Python异常,它是pybind11捕获并转化的C++异常。noexcept函数不能传播这些异常。我们可以将它们转换为Python异常,然后丢弃discard_as_unraisable,如下所示。

  1. void nonthrowing_func() noexcept(true) {
  2. try {
  3. // ...
  4. } catch (py::error_already_set &eas) {
  5. // Discard the Python error using Python APIs, using the C++ magic
  6. // variable __func__. Python already knows the type and value and of the
  7. // exception object.
  8. eas.discard_as_unraisable(__func__);
  9. } catch (const std::exception &e) {
  10. // Log and discard C++ exceptions.
  11. third_party::log(e);
  12. }
  13. }