pybind11 升级指南:从 v2.0 到 v3.0 的迁移路线、破坏性变更与实战要点
【免费下载链接】pybind11Seamless operability between C++11 and Python项目地址: https://gitcode.com/GitHub_Trending/py/pybind11
本文是 pybind11 官方 docs/upgrade.rst 升级指南的深度解读,它配套 changelog 使用:changelog 罗列新特性、改进与修复的完整清单,而升级指南只聚焦真正影响你升级体验的那部分——被弃用的 API 及其替代方案、构建系统变更、代码现代化建议等。读完本文,你将掌握从 pybind11 v2.0 一路升级到 v3.0 需要知道的全部破坏性变更、推荐迁移路径(包括py::smart_holder、py::native_enum等新特性的采用时机),并能用预处理器条件编译兼容新旧两代版本。
升级指南在 pybind11 文档体系中的定位
升级指南是 changelog 的“伴侣文档”。它的价值在于:changelog 告诉你“加了什么”,升级指南告诉你“你升级后需要改什么”。指南中的每一个条目都对应真实的源码变更,例如文档中提到的特性宏可以在 include/pybind11/detail/common.h 中找到定义:
#define PYBIND11_HAS_INTERNALS_WITH_SMART_HOLDER_SUPPORT 1 #define PYBIND11_HAS_NATIVE_ENUM 1这两个宏正是升级指南 v3.0 部分提到的两个预处理器守卫(PYBIND11_HAS_NATIVE_ENUM与PYBIND11_HAS_INTERNALS_WITH_SMART_HOLDER_SUPPORT),它们由新版本头文件自动定义,用于条件编译兼容代码。最新版的完整弃用清单可查阅 docs/advanced/deprecated.rst。
升级到 v3.0:主要新特性与迁移要点
pybind11 v3.0 引入了重大新特性,但绝大多数现有扩展无需修改即可构建运行。只有极少数情况需要小幅调整,且这些调整可以很容易地用预处理器条件编译包裹起来,以保持与 2.x 系列的兼容。
ABI 兼容性与全量重编译建议
扩展模块的 ABI 不兼容是 v3.0 升级中最需要留意的一点:由于新特性与现代化改动,用 pybind11 v3.0 构建的扩展与用 v2.13 构建的扩展之间不保持 ABI 兼容。为保证跨扩展模块兼容,官方建议用 v3.0重新构建所有基于 pybind11 的扩展。
跨扩展模块 ABI 兼容性的处理在 v3.0 中经历了一次重大现代化:新实现能比旧版本更精确地反映真实的 ABI 兼容程度,但细节微妙而复杂。
CMake:切换到现代 FindPython 模块
v3.0 的 CMake 支持默认采用现代FindPython模块。如果你还没更新,pybind11 对旧的PYTHON_*变量提供了部分向后兼容,但你应该切换到使用Python_*变量。注意:设置PYTHON_*变量不再影响构建。
实际的兼容逻辑在 tools/pybind11Common.cmake 中:当PYBIND11_FINDPYTHON未定义、等于"COMPAT"或为真时,pybind11 才会走新的FindPython路径;"COMPAT"模式会打印提示信息并把Python_*变量映射回PYTHON_*以保持兼容。推荐做法是显式设置:
set(PYBIND11_FINDPYTHON ON)这个选项已被支持多年,设置后可以避免进入兼容模式(也就避免了兼容模式警告)。
py::smart_holder 与 py::classh:智能指针持有者的现代化
v3.0 的一大新特性是集成了py::smart_holder,它改善了对std::unique_ptr和std::shared_ptr的支持,解决了一系列长期存在的问题(详见 docs/advanced/classes.rst 中的 smart holder 章节)。与之紧密相关的是新增的py::trampoline_self_life_support(详见 docs/advanced/classes.rst 中 virtual 覆盖章节,头文件为 include/pybind11/trampoline_self_life_support.h)。
为了便于快速尝试py::smart_holder,pybind11 提供了py::classh快捷键。其定义位于 include/pybind11/pybind11.h:
// py::classh<Pet> 是 py::class_<Pet, py::smart_holder> 的简写 using classh = class_<type_, smart_holder, options...>;例如:
py::classh<Pet>(m, "Pet") // 等价于 py::class_<Pet, py::smart_holder>(m, "Pet")py::classh的设计意图是让你在不引入大量空白差异(whitespace changes)的前提下轻松试验py::smart_holder。在很多情况下,把代码里的py::class_全局替换为py::classh是一个有效的第一步:
- 构建失败会迅速暴露出需要移除
std::shared_ptr<...>holder 的位置; - 运行期失败(假设有良好的单元测试覆盖)会突出需要协同修改的基类-派生类场景。
注意 include/pybind11/stl_bind.h 中的py::bind_vector与py::bind_map有一个holder_type模板参数,默认是std::unique_ptr。如果需要py::smart_holder的功能,请显式指定,例如:
py::bind_vector<VecType, py::smart_holder>(m, "VecType");py::native_enum:现代枚举绑定 API
v3.0 新增py::native_enum(头文件 include/pybind11/native_enum.h),用于把 C++ 枚举暴露为 Python 原生类型——通常是标准库的enum.Enum或其子类。相比旧的(现已弃用的)py::enum_,它与 Python 的枚举体系集成得更好。
两个重要注意点:
- 必须显式引入头文件:
#include <pybind11/native_enum.h>不会被自动包含; - 弃用声明:2.x 系列中产生弃用警告的任何内容都可能在 3.x 的未来小版本中被移除,大部分在 3.0 中仍然保留以缓解过渡。
绑定函数现在支持 pickle
使用 pybind11 暴露的函数现在可被 pickle,这移除了一个长期存在的障碍——依赖 pickle 的 Python 特性(如 multiprocessing、缓存工具)之前无法直接使用 pybind11 绑定的函数。
自定义 type caster 的模板特化需求(潜在障碍)
以下问题极不可能出现,但一旦出现也很容易绕开:
场景一:C++ 枚举通过自定义 type caster 绑定到 Python。如果自定义 type caster 是模板化的,可能需要如下模板特化:
#if defined(PYBIND11_HAS_NATIVE_ENUM) namespace pybind11::detail { template <typename FancyEnum> struct type_caster_enum_type_enabled< FancyEnum, enable_if_t<is_fancy_enum<FancyEnum>::value>> : std::false_type {}; } #endifPYBIND11_HAS_NATIVE_ENUM守卫仅在需要向后兼容 pybind11 v2 时才需要。
场景二:自定义了pybind11::detail::copyable_holder_caster或pybind11::detail::move_only_holder_caster实现,且用于std::shared_ptr/std::unique_ptr转换(注意:这两个 caster 从未被正式文档化,虽然自 2017 年起就存在)。此时可能需要:
#if defined(PYBIND11_HAS_INTERNALS_WITH_SMART_HOLDER_SUPPORT) namespace pybind11::detail { template <typename ExampleType> struct copyable_holder_caster_shared_ptr_with_smart_holder_support_enabled< ExampleType, enable_if_t<is_example_type<ExampleType>::value>> : std::false_type {}; } #endif#if defined(PYBIND11_HAS_INTERNALS_WITH_SMART_HOLDER_SUPPORT) namespace pybind11::detail { template <typename ExampleType> struct move_only_holder_caster_unique_ptr_with_smart_holder_support_enabled< ExampleType, enable_if_t<is_example_type<ExampleType>::value>> : std::false_type {}; } #endifPYBIND11_HAS_INTERNALS_WITH_SMART_HOLDER_SUPPORT守卫仅在需要向后兼容 pybind11 v2 时才需要。
官方迁移建议
官方建议尽快迁移到 v3.0,同时把初始改动保持在最小。大多数项目只需更新 pybind11 版本即可升级,无需改动既有绑定代码。经过一段短暂的稳定期(足以暴露任何细微问题)后,再按需增量采用新特性:
- 按需使用
py::smart_holder与py::trampoline_self_life_support改善代码健康度;py::classh是快速试验的捷径; - 逐步从
py::enum_迁移到py::native_enum,改善与 Python 标准枚举类型的集成; - 没有紧迫性去重构已经正常工作的绑定——按需或随维护工作采用新特性即可;
- 如果使用 CMake,更新为
Python_*变量,并尽量设置set(PYBIND11_FINDPYTHON ON)。
旧版本升级要点汇总(v2.12 ~ v2.0)
v2.12:NumPy 2.x 支持
NumPy 支持已升级到 2.x 系列,两个相关变化:
dtype.flags()现在是uint64,dtype.alignment()是ssize_t(NumPy 2.x 中itemsize()可能返回超出整数范围的值);- 长期弃用的
PyArray_GetArrayParamsFromObject不再可用。
更直接的改变是:默认整型"int_"(以及"uint")现在是ssize_t而不是long(影响 64 位 Windows)。如需暂时只支持 NumPy 1.x,可定义PYBIND11_NUMPY_1_ONLY禁用新支持——但必须在所有 pybind11 编译单元上一致地定义,否则可能导致 ODR 违规。该选项未来会被移除,强烈建议尽快适配代码。
需要特别提醒:在本仓库当前版本中,PYBIND11_NUMPY_1_ONLY已经不再支持——include/pybind11/numpy.h 中定义了该宏时会直接报编译错误(参见 PR #5595)。因此旧版升级指南中的这个逃生舱口在新版本中已失效,迁移到 NumPy 2.x 是唯一路径。
v2.11:CMake 最低版本要求
最低 CMake 版本提升到 3.5。注意 CMake 3.27 移除了长期弃用的FindPythonInterp支持(如果你把 3.27 设为最小或最大支持版本)。为未来做准备,强烈推荐 CMake 3.15+ 配合FindPython或设置PYBIND11_FINDPYTHON;否则 pybind11 会在FindPythonInterp不可用时自动切换到FindPython。这正是后续 v3.0 默认行为的铺垫。
v2.9:命名空间与 caster 名称
py::make_simple_namespace的用法应改为py::module_::import("types").attr("SimpleNamespace");- 自定义 type caster 中的
_可用更可读的const_name替代(旧_快捷键保留,除非用作宏如 gettext)。
v2.7:py::str 的严格化
v2.7 之前,py::str可以持有PyUnicodeObject或PyBytesObject,py::isinstance<str>()对两者都返回true。从 v2.7 起,py::str只持有PyUnicodeObject,py::isinstance<str>()只对py::str为true。PYBIND11_STR_LEGACY_PERMISSIVE宏作为逃生舱口可恢复旧行为(在 include/pybind11/detail/common.h 中有注释说明,未来会被移除)。两类常见修复:
- 被旧行为掩盖的
py::str/py::bytes混用——把py::str改成py::bytes即可; - 依赖
py::isinstance<str>(obj)对py::bytes为真——多数情况加|| py::isinstance<bytes>(obj)即可,若出现在模板中则需仔细审查并定制修复。
v2.6:命名规范与行为收紧
- 宏更名:
PYBIND11_OVERLOAD*和get_overload应替换为PYBIND11_OVERRIDE*和get_override(docs/advanced/classes.rst 中明确说明更名发生在 v2.5.0 左右,旧名称未来可能被移除); - 模块类型更名:
py::module更名为py::module_(保留向后兼容 typedef)。原因是 C++20 语言规则要求未限定的module不能出现在逻辑行行首; - 构造函数弃用:
py::module_的公开构造函数被弃用,改用PYBIND11_MODULE或module_::create_extension_module; - 行为收紧:子类忘记调用
__init__现在会抛错;向子类做非法转换(如从py::object转py::bytes)现在抛py::type_error; - API 调整:未文档化的
h.get_type()弃用,改用py::type::of(h);枚举预定义__str__,要覆盖时在定义"__str__"处加py::prepend()标签;定义__eq__而未定义__hash__时__hash__会被置为None(与 CPython 一致),需要哈希则用py::hash快捷键;py::array构造函数尺寸统一为有符号整数,可能引发编译警告,请转为py::ssize_t; - 工具迁移:
tools/clang子模块和tools/mkdoc.py迁移到独立的 pybind11-mkdoc 包; - wheel 头文件槽位:PyPI 上的 pybind11 包不再填充 wheel 的 "headers" 槽位,需要时可
pip install "pybind11[global]"。多数用户不受影响,因为python -m pybind11 --includes与pybind11.get_include()自 2.5 起一直正确指向pybind11/include。
v2.6 的 CMake 变更(重要):
PYBIND11_CPP_STANDARD=<平台标志>弃用,改用CMAKE_CXX_STANDARD=<数字>或target_compile_features;- 未显式要求标准时,pybind11 目标使用编译器默认标准(不低于 C++11),不再强制 C++14。依赖旧行为的请用
set(CMAKE_CXX_STANDARD 14 CACHE STRING ""); pybind11::module的直接使用应配合set(CMAKE_CXX_VISIBILITY_PRESET hidden)或类似设置;pybind11_add_module的SYSTEM参数弃用且无效果,链接行为与其它导入库一致(默认按SYSTEM库处理);- 未设置
PYTHON_EXECUTABLE时,虚拟环境(venv、virtualenv、conda)优先于标准搜索; CMAKE_INTERPROCEDURAL_OPTIMIZATION若已设置会被pybind11_add_module尊重,替代链接pybind11::lto/pybind11::thin_lto;- 在 pybind11 之前使用
find_package(Python COMPONENTS Interpreter Development)会让 pybind11 使用新的 Python 机制而非自定义搜索,未来可能成为默认。
v2.5:头文件随 Python 包分发
Python 包现在把头文件作为数据包含在包自身中(同时也放在 "headers" wheel 槽位)。pybind11 --includes与pybind11.get_include()报告新位置,无论安装方式如何都始终正确,旧user=参数失去意义。
v2.2:模块入口宏与符号可见性
PYBIND11_PLUGIN宏弃用,PYBIND11_MODULE成为首选:
// old PYBIND11_PLUGIN(example) { py::module m("example", "documentation string"); m.def("add", [](int a, int b) { return a + b; }); return m.ptr(); } // new PYBIND11_MODULE(example, m) { m.doc() = "documentation string"; // optional m.def("add", [](int a, int b) { return a + b; }); }自定义构造函数与 pickle 的新 API:旧的 placement-new 自定义构造函数弃用,新方式用py::init()与工厂函数,显著提升类型安全(placement-new 可能意外用不兼容类型调用,或在不谨慎的 Python 侧__init__调用下重复初始化同一对象)。详见 docs/advanced/classes.rst 的自定义构造器与 pickling 章节:
// old -- deprecated (runtime warning shown only in debug mode) py::class<Foo>(m, "Foo") .def("__init__", [](Foo &self, ...) { new (&self) Foo(...); // uses placement-new }); // new py::class<Foo>(m, "Foo") .def(py::init([](...) { // Note: no `self` argument return new Foo(...); // return by raw pointer // or: return std::make_unique<Foo>(...); // return by holder // or: return Foo(...); // return by value (move constructor) }));pickle 同理,py::pickle()成为首选:
// old -- deprecated (runtime warning shown only in debug mode) py::class<Foo>(m, "Foo") ... .def("__getstate__", [](const Foo &self) { return py::make_tuple(self.value1(), self.value2(), ...); }) .def("__setstate__", [](Foo &self, py::tuple t) { new (&self) Foo(t[0].cast<std::string>(), ...); }); // new py::class<Foo>(m, "Foo") ... .def(py::pickle( [](const Foo &self) { // __getstate__ return py::make_tuple(self.value1(), self.value2(), ...); // unchanged }, [](py::tuple t) { // __setstate__, note: no `self` argument return new Foo(t[0].cast<std::string>(), ...); // or: return std::make_unique<Foo>(...); // return by holder // or: return Foo(...); // return by value (move constructor) } ));构造与 pickle 的警告在模块初始化时(import 时而非函数调用时)显示,且只在 debug 模式下可见。示例警告:
pybind11-bound class 'mymodule.Foo' is using an old-style placement-new '__init__' which has been deprecated. See the upgrade guide in pybind11's docs.符号隐藏的严格化:pybind11 从 v2.2 起更严格地强制模块隐藏符号:一是声明pybind11命名空间内所有符号为隐藏,二是在 Linux/macOS 上自动附带-fvisibility=hidden标志(仅针对扩展模块,不影响内嵌解释器)。这样确保:不同 pybind11 版本编译的模块互不冲突;py::module_local绑定等新特性按预期工作。在 CMake 构建系统中,pybind11_add_module以前只在 release 模式设置该标志,现在无条件应用且不能用NO_EXTRAS取消;pybind11::module目标也把该标志加进接口(pybind11::embed不变)。
如果你的 Python 模块同时充当共享库(有依赖者),需要手动导出符号,或把共享库拆出来。临时恢复默认可见性的方法(不推荐长期使用):
target_link_libraries(mymodule PRIVATE pybind11::module) add_library(restore_default_visibility INTERFACE) target_compile_options(restore_default_visibility INTERFACE -fvisibility=default) target_link_libraries(mymodule PRIVATE restore_default_visibility)本地 STL 容器绑定:旧版只能全局绑定类型——所有模块共享同一导出类型,两个模块导出相同 C++ 类型(尤其是std::vector<int>这类常见类型)会冲突。py::module_local用于解决此问题(完整用法见 docs/advanced/classes.rst 的 module_local 章节,STL 绑定细节见 docs/advanced/cast/stl.rst)。py::class_仍默认全局绑定,但py::bind_vector和py::bind_map在元素为内置类型、未用py::class_绑定或绑定为py::module_local时,会把 STL 容器绑定为py::module_local——这让多个模块可各自绑定std::vector<int>而不冲突。
升级时注意:模块间 C++→Python 方向的转换会受本地化影响(Python→C++ 方向仍可接受外来py::module_local类型)。若多个模块需要共享单个全局 STL 绑定,要么在所有需要的模块中各加一份相同的 STL 绑定,要么用py::module_local(false)恢复该绑定的全局状态。
负步幅支持:负步幅要求py::buffer_info与py::array接口的整型从无符号改为有符号。启用编译警告后可能看到新的转换警告,用static_cast消除即可。
部分 py::object API 弃用:指针比较用obj1.is(obj2)(等价于 Python 的obj1 is obj2),旧operator==弃用;borrowed/stolen构造标签改为直接使用borrowed_t{}/stolen_t{}。
编译期错误检查更严格:std::shared_ptr<T>的自动转换在T未直接注册到py::class_<T>时不可行(如std::shared_ptr<int>不能自动转换),绑定这类参数现在直接编译报错。py::init<...>()也更严格,阻止可能引发意外行为的绑定:
struct Example { Example(int &); }; py::class_<Example>(m, "Example") .def(py::init<int &>()); // OK, exact match // .def(py::init<int>()); // compile-time error, mismatch非const左值引用不能绑定右值;但const T &构造函数仍可用py::init<T>()注册,因为const左值引用可以绑定右值。
v2.1:编译器版本与静态属性
最低编译器版本在编译期强制检查(v2.0 已有要求,v2.1 起显式报错):GCC >= 4.8、clang >= 3.3(appleclang >= 5.0)、MSVC >= 2015u3、Intel C++ >= 15.0。
静态属性不再需要 py::metaclass:绑定类默认支持静态属性,零参数的py::metaclass()弃用;新增一参数py::metaclass(python_type)用于少数需要自定义元类覆盖 pybind11 默认值的场景:
// old -- emits a deprecation warning py::class_<Foo>(m, "Foo", py::metaclass()) .def_property_readonly_static("foo", ...); // new -- static properties work without the attribute py::class_<Foo>(m, "Foo") .def_property_readonly_static("foo", ...); // new -- advanced feature, override pybind11's default metaclass py::class_<Bar>(m, "Bar", py::metaclass(custom_python_type)) ...v2.0:py::class_ 的破坏性变更
v2.0 的变更是为了支撑 PyPy 的 cpyext 机制、提升效率,以及让类型定义面向未来:
- buffer protocol 必须显式声明:提供 buffer 协议访问的类型现在必须在
py::class_构造参数中带py::buffer_protocol():
py::class_<Matrix>("Matrix", py::buffer_protocol()) .def(py::init<...>()) .def_buffer(...);静态属性曾需要 py::metaclass():此要求在 v2.1 已移除。若从 1.x 升级,建议直接跳到 v2.1 或更新版本。
trampoline 语法变化:v1.x 的
.alias<MyClass>()改为在py::class_模板中同时指定原类与 trampoline 类:
// old v1.x syntax py::class_<TrampolineClass>("MyClass") .alias<MyClass>() ... // new v2.x syntax py::class_<MyClass, TrampolineClass>("MyClass") ...原类必须是py::class_的第一个模板参数,trampoline 可与其他参数(基类、holder)任意顺序混合。新方案在 Python 不覆盖任何 C++ 函数时零开销。
py::base<T>() 弃用:改为把基类作为py::class_模板参数,天然支持多重继承:
// old v1.x py::class_<Derived>("Derived", py::base<Base>()); // new v2.x py::class_<Derived, Base>("Derived"); // new -- multiple inheritance py::class_<Derived, Base1, Base2>("Derived"); // new -- apart from `Derived` the argument order can be arbitrary py::class_<Derived, Base1, Holder, Base2, Trampoline>("Derived");std::shared_ptr 开箱即用:相关 type caster 已内置,不再需要PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr<T>)(保留该声明也不会报错或警告,但完全冗余)。
py::object API 弃用对照表(旧写法均会产生弃用警告):
| 旧语法 | 新语法 |
|---|---|
obj.call(args...) | obj(args...) |
obj.str() | py::str(obj) |
auto l = py::list(obj); l.check() | py::isinstance<py::list>(obj) |
py::object(ptr, true) | py::reinterpret_borrow<py::object>(ptr) |
py::object(ptr, false) | py::reinterpret_steal<py::object>(ptr) |
if (obj.attr("foo")) | if (py::hasattr(obj, "foo")) |
if (obj["bar"]) | if (obj.contains("bar")) |
实战迁移清单
综合各版本要点,一份可操作的升级检查清单如下:
- 先升级版本、暂不动代码:更新 pybind11 版本后构建,用编译错误清单驱动修改;
- CMake 层:
PYTHON_*→Python_*,设置set(PYBIND11_FINDPYTHON ON),确保 C++ 标准通过CMAKE_CXX_STANDARD指定; - 全量重编译:v3.0 与 v2.13 不 ABI 兼容,所有扩展模块需用 v3.0 重建;
- 实验性采用新特性:全局替换
py::class_→py::classh快速试探smart_holder;py::enum_→py::native_enum(记得显式#include <pybind11/native_enum.h>); - 处理自定义 caster 特化:如需兼容 v2,用
PYBIND11_HAS_NATIVE_ENUM与PYBIND11_HAS_INTERNALS_WITH_SMART_HOLDER_SUPPORT守卫包裹特化; - 关注宏名与 API 更名:
PYBIND11_OVERLOAD*→PYBIND11_OVERRIDE*、py::module→py::module_、get_type()→py::type::of(); - 清理旧式构造/pickle:placement-new 的
__init__/__getstate__/__setstate__迁移到py::init工厂与py::pickle(); - 符号可见性:确认构建系统已应用
-fvisibility=hidden,模块若同时是共享库需显式导出符号。
本文所有结论均以当前仓库中的 docs/upgrade.rst、include/pybind11/detail/common.h、include/pybind11/pybind11.h、include/pybind11/native_enum.h、include/pybind11/numpy.h、tools/pybind11Common.cmake 及 docs/changelog.md 为事实依据。若要查看这些变更在真实项目中的验证方式,可参考仓库 tests 目录下对应的测试用例,例如 tests/test_native_enum.cpp 与 tests/test_class_sh_basic.cpp,它们分别覆盖了py::native_enum与smart_holder的运行时行为。
【免费下载链接】pybind11Seamless operability between C++11 and Python项目地址: https://gitcode.com/GitHub_Trending/py/pybind11
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考