news 2026/9/13 16:02:21

pybind11 异常处理完全指南:C++ 与 Python 异常的双向翻译机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
pybind11 异常处理完全指南:C++ 与 Python 异常的双向翻译机制

pybind11 异常处理完全指南:C++ 与 Python 异常的双向翻译机制

【免费下载链接】pybind11Seamless operability between C++11 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::exceptionRuntimeError
std::bad_allocMemoryError
std::domain_errorValueError
std::invalid_argumentValueError
std::length_errorValueError
std::out_of_rangeIndexError
std::range_errorValueError
std::overflow_errorOverflowError
pybind11::stop_iterationStopIteration(用于实现自定义迭代器)
pybind11::index_errorIndexError(用于在__getitem____setitem__等中标记越界访问)
pybind11::key_errorKeyError(用于类字典对象在__getitem____setitem__中的越界/缺键访问)
pybind11::value_errorValueError(例如container.remove(...)传入错误值时)
pybind11::type_errorTypeError
pybind11::buffer_errorBufferError
pybind11::import_errorImportError
pybind11::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_exception<CppExp>(module, "PyExp");

这一调用会在给定模块中创建一个名为PyExp的 Python 异常类,并自动将遇到的CppExp类型异常转换为PyExp

对应的局部版本:

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

第三个参数可以是一个handle,用于指定新异常类的基类

py::register_exception<CppExp>(module, "PyExp", PyExc_RuntimeError); py::register_local_exception<CppExp>(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::exception<CppException>对象(保证 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以避免捕获)。

下面的示例演示了对假设异常类MyCustomExceptionOtherException的翻译:前者翻译成自定义 Python 异常MyCustomError,后者翻译成标准RuntimeError(这也是官方文档给出的完整示例):

PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object> exc_storage; exc_storage.call_once_and_store_result( [&]() { return py::exception<MyCustomException>(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_1MyException5的子类,两者分别由std::logic_error及其子类翻译而来:

auto ex5 = py::register_exception<MyException5>(m, "MyException5"); py::register_exception<MyException5_1>(m, "MyException5_1", ex5.ptr());

两个重要的注意事项(官方 note)

  1. 每个 catch 子句都必须调用py::set_error()。漏调会导致 Python 以SystemError: error return without exception set崩溃。不打算处理的异常干脆不要 catch,或显式重新抛出,交给其他先前声明的翻译器。set_error有两个重载,定义在 pytypes.h,分别对应PyErr_SetString(消息字符串)和PyErr_SetObject(异常对象)。
  2. 跨 ABI 边界导出异常:在 macOS 上,libc++libstdc++-fvisibility=hidden下行为不同,因此跨 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 won't 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 from

Python 有表示"一个异常由另一个异常引起"的机制:

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_errorraise_from。该功能自 pybind11 2.8 加入(versionadded 2.8)。tests/test_exceptions.cpp 中有对应的绑定示例raise_fromraise_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 的异常体系可以归纳为四条主线:

  1. C++ → Python:内置映射表 + 可插拔翻译器(局部优先、逆序尝试),翻译器中每个 catch 子句必须set_error
  2. Python → C++:一律收敛为error_already_set,用matches做类型判断,原样throw即可继续传播;
  3. C API 错误协议throw py::error_already_set()PyErr_Clear(),二选一不可遗漏;
  4. 边界场景raise_from构建异常链(2.8+),discard_as_unraisable消化析构/noexcept路径中的异常(2.6+)。

配套验证材料集中在 tests/test_exceptions.cpp(翻译器注册与委托的完整示例)、tests/test_exceptions.h(跨模块共享异常与PYBIND11_EXPORT_EXCEPTION导出)以及 tests/test_exceptions.py(Python 侧断言),可作为编写自有绑定代码时异常处理实现的事实参照。

【免费下载链接】pybind11Seamless operability between C++11 and Python项目地址: https://gitcode.com/GitHub_Trending/py/pybind11

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 15:57:39

OSPF动态路由协议详解与配置实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 15:55:35

cuML源码快照评估:从工程结构判断GPU机器学习库的PoC可行性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 15:54:23

私有化CI/CD选型:GitLab Self-Managed vs 腾讯云CNB企业版深度对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 15:51:16

Unity接入MediaPipe姿态数据的轻量级实时方案

简介&#xff1a;本资源是一套基于Python与MediaPipe在Unity引擎中实现人体姿态追踪的完整实践方案&#xff0c;面向Unity初学者、计算机视觉入门者及跨领域项目开发者&#xff0c;解决多平台姿态数据实时采集与Unity可视化集成的技术难点。资源包共7个文件&#xff0c;包含2个…

作者头像 李华
网站建设 2026/9/13 15:51:13

Migrate Customer-Facing API to GraphQL

Migrate Customer-Facing API to GraphQL 【免费下载链接】skills Skills Catalog for Codex 项目地址: https://gitcode.com/GitHub_Trending/skills4/skills Context Our REST API has grown to 50 endpoints with inconsistent patterns... Decision Migrate cust…

作者头像 李华