
pybind11 异常处理完全指南C 与 Python 异常的双向翻译机制【免费下载链接】pybind11Seamless operability between C11 and Python项目地址: https://gitcode.com/GitHub_Trending/py/pybind11本篇指南以 pybind11 官方文档 docs/advanced/exceptions.rst 为主体系统讲解 pybind11 在 C 与 Python 之间双向转换异常的完整机制内置异常映射表、自定义异常翻译器全局/局部、error_already_set的捕获与判断、Python C API 错误协议、警告warning封装、异常链raise from以及不可抛unraisable异常的处理。读完之后你将能够在绑定代码中实现生产级的错误处理并理解其背后 exception_translation.h 等源码中的执行路径。内置的 C 到 Python 异常翻译当 Python 通过 pybind11 调用 C 代码时pybind11 内置了一个 C 异常处理器它捕获 C 异常、将其翻译成对应的 Python 异常并抛出使 Python 侧代码可以正常try/except处理。pybind11 为std::exception及其标准子类以及一组专门定义的异常类注册了翻译规则。注意这些pybind11::xxx_error并不是真正的 Python 异常不能用 Python C API 检查它们它们是纯 C 对象只有在到达 pybind11 的异常处理器时才被翻译成对应的 Python 异常。默认异常映射表C 抛出的异常翻译成的 Python 异常类型std::exceptionRuntimeErrorstd::bad_allocMemoryErrorstd::domain_errorValueErrorstd::invalid_argumentValueErrorstd::length_errorValueErrorstd::out_of_rangeIndexErrorstd::range_errorValueErrorstd::overflow_errorOverflowErrorpybind11::stop_iterationStopIteration用于实现自定义迭代器pybind11::index_errorIndexError用于在__getitem__、__setitem__等中标记越界访问pybind11::key_errorKeyError用于类字典对象在__getitem__、__setitem__中的越界/缺键访问pybind11::value_errorValueError例如container.remove(...)传入错误值时pybind11::type_errorTypeErrorpybind11::buffer_errorBufferErrorpybind11::import_errorImportErrorpybind11::attribute_errorAttributeError其他任意异常RuntimeError从源码结构看上表右半部分的所有异常类都由一个宏统一生成定义在 common.h 中。基类builtin_exception继承自std::runtime_error并声明了一个纯虚函数set_error()宏PYBIND11_RUNTIME_EXCEPTION(name, type)为每个名字生成对应类其set_error()的实现就是PyErr_SetString(type, what())——把 C 异常的what()文本写入指定 Python 异常/// C bindings of builtin Python exceptions class PYBIND11_EXPORT_EXCEPTION builtin_exception : public std::runtime_error { public: using std::runtime_error::runtime_error; /// Set the error using the Python C API virtual void set_error() const 0; }; #define PYBIND11_RUNTIME_EXCEPTION(name, type) \ class PYBIND11_EXPORT_EXCEPTION name : public builtin_exception { \ public: \ using builtin_exception::builtin_exception; \ name() : name() {} \ void set_error() const override { PyErr_SetString(type, what()); } \ }; PYBIND11_RUNTIME_EXCEPTION(stop_iteration, PyExc_StopIteration) PYBIND11_RUNTIME_EXCEPTION(index_error, PyExc_IndexError) PYBIND11_RUNTIME_EXCEPTION(key_error, PyExc_KeyError) PYBIND11_RUNTIME_EXCEPTION(value_error, PyExc_ValueError) PYBIND11_RUNTIME_EXCEPTION(type_error, PyExc_TypeError) PYBIND11_RUNTIME_EXCEPTION(buffer_error, PyExc_BufferError) PYBIND11_RUNTIME_EXCEPTION(import_error, PyExc_ImportError) PYBIND11_RUNTIME_EXCEPTION(attribute_error, PyExc_AttributeError) PYBIND11_RUNTIME_EXCEPTION(cast_error, PyExc_RuntimeError)其中还定义了特殊的cast_error它由handle::call在输入参数无法转换为 Python 对象时抛出。一个关键的方向性问题异常翻译不是双向的。在 C 中catch上述 C 异常如py::value_error不会捕获到源自 Python 的异常。要捕获 Python 抛出的异常必须catch (py::error_already_set)详见本文后半部分。注册自定义异常翻译器如果默认的转换策略不够用pybind11 支持注册自定义异常翻译器。与 pybind11 类注册类似翻译器可以是局部的local仅对其定义所在的模块生效或全局的作用于整个 Python 会话。简单注册py::register_exception对于“把某个 C 异常翻译成一个新的 Python 异常消息直接取what()”这类简单场景pybind11 提供了辅助函数py::register_exceptionCppExp(module, PyExp);这一调用会在给定模块中创建一个名为PyExp的 Python 异常类并自动将遇到的CppExp类型异常转换为PyExp。对应的局部版本py::register_local_exceptionCppExp(module, PyExp);第三个参数可以是一个handle用于指定新异常类的基类py::register_exceptionCppExp(module, PyExp, PyExc_RuntimeError); py::register_local_exceptionCppExp(module, PyExp, PyExc_RuntimeError);这样PyExp既可以按PyExp捕获也可以按RuntimeError捕获。内置 Python 异常类对象的完整列表见 Python 官方文档中的 Standard Exceptions 一节默认基类是PyExc_Exception。从源码看上述两个函数最终都汇入 pybind11.h 中的register_exception_impl它先用gil_safe_call_once_and_store存储创建出的py::exceptionCppException对象保证 GIL 安全地只创建一次然后注册一个翻译器 lambda——翻译器内用std::rethrow_exception(p)重新抛出catch (const CppException e)命中后调用set_error(exc_storage.get_stored(), e.what())。py::exception类本身pybind11.h通过PyErr_NewException创建带模块名.异常名完整命名空间的 Python 异常类并将其挂到模块属性上重复定义同名属性会直接触发pybind11_fail。高级翻译器register_exception_translator需要更复杂的翻译逻辑时使用py::register_exception_translator(translator)或py::register_local_exception_translator(translator)注册一个函数。翻译器是一个无状态可调用对象函数指针或不捕获变量的 lambda调用签名为void(std::exception_ptr)。翻译规则如下与源码 exception_translation.h 中try_translate_exceptions的注释一致C 异常被抛出时已注册的翻译器按注册的逆序尝试最后注册的翻译器最先获得处理机会局部翻译器全部先于全局翻译器尝试一个翻译器可以捕获异常并调用py::set_error()什么都不做让异常落到下一个翻译器或抛出一种新类型的异常把翻译委托给先前注册的翻译器。翻译器内部应在 try 块中用std::rethrow_exception重新抛出异常然后为一个或多个合适的 catch 子句各调用一次py::set_error()。要声明自定义 Python 异常类型声明一个py::exception变量并在配套翻译器中使用它在 lambda 中使用时通常写成static以避免捕获。下面的示例演示了对假设异常类MyCustomException和OtherException的翻译前者翻译成自定义 Python 异常MyCustomError后者翻译成标准RuntimeError这也是官方文档给出的完整示例PYBIND11_CONSTINIT static py::gil_safe_call_once_and_storepy::object exc_storage; exc_storage.call_once_and_store_result( []() { return py::exceptionMyCustomException(m, MyCustomError); }); py::register_exception_translator([](std::exception_ptr p) { try { if (p) std::rethrow_exception(p); } catch (const MyCustomException e) { py::set_error(exc_storage.get_stored(), e.what()); } catch (const OtherException e) { py::set_error(PyExc_RuntimeError, e.what()); } });单个翻译器可以处理多种异常如上例所示。如果当前翻译器没有捕获异常先前注册的翻译器会得到机会如果没有任何翻译器能处理异常会落入默认转换器即上一节的默认策略。若连默认转换器都逃逸exception_translation.h 会兜底设置SystemError(Exception escaped from default exception translator!)。测试代码中的真实案例官方文档特别指出 tests/test_exceptions.cpp 包含各种自定义翻译器与自定义异常类型的示例其中两个细节值得注意委托delegation——test_exceptions.cpp 中注册了一个针对MyException4的翻译器它不直接设置 Python 错误而是throw MyException(e.what())把翻译权委托给之前为MyException注册的翻译器这正是抛出新类型异常以委托的实现方式// register new translator for MyException4 // which will catch it and delegate to the previously registered // translator for MyException by throwing a new exception py::register_exception_translator([](std::exception_ptr p) { try { if (p) { std::rethrow_exception(p); } } catch (const MyException4 e) { throw MyException(e.what()); } });异常继承层次——test_exceptions.cpp 用第三个参数让 Python 侧的异常也形成继承关系MyException5_1是MyException5的子类两者分别由std::logic_error及其子类翻译而来auto ex5 py::register_exceptionMyException5(m, MyException5); py::register_exceptionMyException5_1(m, MyException5_1, ex5.ptr());两个重要的注意事项官方 note每个 catch 子句都必须调用py::set_error()。漏调会导致 Python 以SystemError: error return without exception set崩溃。不打算处理的异常干脆不要 catch或显式重新抛出交给其他先前声明的翻译器。set_error有两个重载定义在 pytypes.h分别对应PyErr_SetString消息字符串和PyErr_SetObject异常对象。跨 ABI 边界导出异常在 macOS 上libc与libstdc在-fvisibilityhidden下行为不同因此跨 ABI 边界使用的异常需要显式导出例如 tests/test_exceptions.h 中共享给多个测试模块的shared_exception使用了PYBIND11_EXPORT_EXCEPTION宏class PYBIND11_EXPORT_EXCEPTION shared_exception : public pybind11::builtin_exception { public: using builtin_exception::builtin_exception; explicit shared_exception() : shared_exception() {} void set_error() const override { py::set_error(PyExc_RuntimeError, what()); } };局部翻译器 vs 全局翻译器全局异常翻译器会按注册逆序应用于所有模块。这会产生一个微妙问题模块导入顺序会影响异常的翻译结果。假设 module1 注册了如下翻译器py::register_exception_translator([](std::exception_ptr p) { try { if (p) std::rethrow_exception(p); } catch (const std::invalid_argument e) { py::set_error(PyExc_ArgumentError, module1 handled this); } });而 module2 注册了一个几乎相同的翻译器py::register_exception_translator([](std::exception_ptr p) { try { if (p) std::rethrow_exception(p); } catch (const std::invalid_argument e) { py::set_error(PyExc_ArgumentError, module2 handled this); } });那么到底由哪个翻译器处理invalid_argument取决于 module1 与 module2 的导入顺序。由于翻译器按注册逆序执行最后被导入的模块会获胜它的翻译器生效。从源码看注册时翻译器被push_front进对应链表见 pybind11.h而try_translate_exceptions按链表顺序遍历、局部链表先于全局链表尝试exception_translation.h这正好实现了逆序注册 局部优先的语义。结论当多个 pybind11 模块共享标准或自定义异常类型加载到同一个 Python 实例中、且需要一致的错误处理行为时应当使用register_local_exception_translator。将上例改为局部翻译器后module2 代码中抛出的invalid_argument总是由 module2 的翻译器处理module1 中的同样总是由 module1 的翻译器处理与导入顺序无关。在 C 中处理 Python 抛出的异常当 C 调用 Python 函数如回调函数、操纵 Python 对象而 Python 抛出Exception时pybind11 会把 Python 异常转换成 C 异常pybind11::error_already_set抛出其载荷中包含一个 C 字符串形式的文本摘要和真正的 Python 异常对象。error_already_set用于把 Python 异常传播回 Python或者在 C 中就地处理。Python 中抛出的异常抛出的 C 异常类型任意 PythonExceptionpybind11::error_already_set例如try { // open(missing.txt, r) auto file py::module_::import(io).attr(open)(missing.txt, r); auto text file.attr(read)(); file.attr(close)(); } catch (py::error_already_set e) { if (e.matches(PyExc_FileNotFoundError)) { py::print(missing.txt not found); } else if (e.matches(PyExc_PermissionError)) { py::print(missing.txt found but not accessible); } else { throw; } }注意这里的 C 到 Python 异常翻译不适用——那是把 C 异常翻译成 Python 的方法方向相反。Python 抛出的错误在 C 侧永远是error_already_set。官方文档用一个指环示例阐明这一点try { py::eval(raise ValueError(The Ring)); } catch (py::value_error boromir) { // Boromir never gets the ring assert(false); } catch (py::error_already_set frodo) { // Frodo gets the ring py::print(I will take the ring); } try { // py::value_error 是请求 pybind11 抛出一个 Python 异常 throw py::value_error(The ball); } catch (py::error_already_set cat) { // cat wont catch the ball since // py::value_error 不是 Python 异常 assert(false); } catch (py::value_error dog) { // dog will catch the ball py::print(Run Spot run); throw; // 再次抛出pybind11 将抛出 ValueError }前半段说明Pythoneval里抛出的ValueError在 C 侧只会被error_already_set捕获py::value_error捕获不到后半段说明反向亦然——throw py::value_error(...)是 C 异常只能被catch (py::value_error)捕获catch (py::error_already_set)捕获不到它。其中matches成员函数可在不移动异常对象的情况下判断其 Python 类型是否匹配某个基类便于在catch中做类型分支。处理来自 Python C API 的错误尽可能使用 pybind11 封装wrappers而非直接调用 Python C API。当确实直接调用 C API 时除了手动管理引用计数外还必须遵循 pybind11 的错误协议调用 Python C API 后如果 Python 返回了错误就throw py::error_already_set();让 pybind11 处理该异常并将其交回 Python 解释器。对py::set_error()这类设置错误的函数同样适用py::set_error(PyExc_TypeError, C API type error demo); throw py::error_already_set(); // 但更简单的写法通常是…… throw py::type_error(pybind11 wrapper type error);另一种选择是忽略该错误调用PyErr_Clear()。任何 Python 错误都必须被抛出或清除否则 Python/pybind11 会停留在无效状态。处理 Python 警告warnings处理 Python 警告的封装位于 warnings.h。注意该头文件必须显式 include不会通过pybind11/pybind11.h传递包含。用warn函数发出警告py::warnings::warn(This is a warning!, PyExc_Warning); // 可选指定 stack_level py::warnings::warn(Another one!, PyExc_DeprecationWarning, 3);从源码看warn的签名为warn(const char *message, handle category PyExc_RuntimeWarning, int stack_level 2)warnings.h底层调用PyErr_WarnEx若类别不是PyExc_Warning的子类会直接pybind11_failPyErr_WarnEx失败则抛出error_already_set。用new_warning_type在模块级别注册新的警告类型py::warnings::new_warning_type(m, CustomWarning, PyExc_RuntimeWarning);其实现warnings.h校验基类必须是PyExc_Warning子类、模块中不存在同名属性后通过PyErr_NewException(模块名.CustomWarning, base, nullptr)创建异常类并挂到模块属性上。异常链raise fromPython 有表示一个异常由另一个异常引起的机制try: print(1 / 0) except Exception as exc: raise RuntimeError(could not divide by zero) from exc在 pybind11 中做类似的事使用py::raise_from函数。它设置当前的 Python 错误指示器因此要继续传播异常应当再throw py::error_already_set()try { py::eval(print(1 / 0)); } catch (py::error_already_set e) { py::raise_from(e, PyExc_RuntimeError, could not divide by zero); throw py::error_already_set(); }raise_from有两个重载均定义在 pytypes.h一个接受已存在的error_already_set作为__cause__另一个先set_error再raise_from。该功能自 pybind11 2.8 加入versionadded 2.8。tests/test_exceptions.cpp 中有对应的绑定示例raise_from与raise_from_already_set可在tests/test_exceptions.py中查看 Python 侧的断言验证。处理不可抛出的异常unraisable exceptions如果一个 Python 函数是从 C析构函数或任何标记为noexcept(true)的函数统称noexcept 函数调用的且它抛出了异常那么异常无处传播——这类函数不允许抛异常。若它们抛出异常或未在其调用图中捕获任何异常C 运行时将调用std::terminate()立即终止进程。类似地类__del__方法中抛出的 Python 异常不会传播但会触发sys.unraisablehook()并记录一条审计audit事件。因此每个 noexcept 函数都应有 try-catch 块来捕获error_already_set或其他可能出现的异常。注意 pybind11 对 Python 异常的封装类如pybind11::value_error不是Python 异常而是 C 异常由 pybind11 捕获并转换成 Python 异常——noexcept 函数同样无法传播它们。一个实用做法是把它们转换成 Python 异常后用discard_as_unraisable丢弃如官方文档所示void nonthrowing_func() noexcept(true) { try { // ... } catch (py::error_already_set eas) { // Discard the Python error using Python APIs, using the C magic // variable __func__. Python already knows the type and value and of the // exception object. eas.discard_as_unraisable(__func__); } catch (const std::exception e) { // Log and discard C exceptions. third_party::log(e); } }discard_as_unraisable在 pytypes.h 中提供了两个重载一个接受object形式的上下文对象另一个接受字符串内部转换为 Python 字符串对象它调用 Python 的PyUnraisableHook相关 API让解释器以__del__异常的方式记录该错误。该功能自 pybind11 2.6 加入versionadded 2.6。tests/test_exceptions.cpp 中的PythonAlreadySetInDestructor正是这一模式的落地示例析构函数中访问不存在的字典键会触发 Python 错误捕获py::error_already_set后调用ex.discard_as_unraisable(s)从而避免std::terminate()。小结pybind11 的异常体系可以归纳为四条主线C → Python内置映射表 可插拔翻译器局部优先、逆序尝试翻译器中每个 catch 子句必须set_errorPython → C一律收敛为error_already_set用matches做类型判断原样throw即可继续传播C API 错误协议throw py::error_already_set()或PyErr_Clear()二选一不可遗漏边界场景raise_from构建异常链2.8discard_as_unraisable消化析构/noexcept路径中的异常2.6。配套验证材料集中在 tests/test_exceptions.cpp翻译器注册与委托的完整示例、tests/test_exceptions.h跨模块共享异常与PYBIND11_EXPORT_EXCEPTION导出以及 tests/test_exceptions.pyPython 侧断言可作为编写自有绑定代码时异常处理实现的事实参照。【免费下载链接】pybind11Seamless operability between C11 and Python项目地址: https://gitcode.com/GitHub_Trending/py/pybind11创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考