1. 项目概述为什么Pybind11 v3.0是Python 3.8开发者的必选项如果你正在用Python做高性能计算、机器学习推理引擎或者想把一个用C写的核心算法库包装成Python模块那你大概率听说过或者正在被pybind11和Python版本兼容性这两个问题来回折磨。特别是当你的项目需要支持Python 3.8、3.9、3.10乃至更新的版本时你会发现老版本的pybind11比如v2.x就像一件不合身的旧衣服编译时各种警告和错误层出不穷运行时也可能出现一些难以捉摸的奇怪行为。我自己就踩过这样的坑一个在Python 3.8上跑得好好的C扩展模块升级到Python 3.10后导入时直接报“ABI不兼容”的错误一夜回到解放前debug过程苦不堪言。这就是我们今天要深入探讨的“pybind11 v3.0新特性实战指南”的核心价值。它不仅仅是一个库的版本更新说明而是一份针对Python 3.8及以上版本生态的“兼容性解决方案”和“性能优化手册”。pybind11 v3.0的发布官方宣称是“为现代Python而构建”这句话背后意味着它对Python内部机制如PEP 384定义的稳定ABI、PEP 489定义的多阶段初始化提供了原生且完善的支持。简单来说v3.0帮你把那些底层的、晦涩的兼容性脏活累活都干了让你能更专注于业务逻辑的C/Python绑定本身。对于开发者而言这意味着什么首先跨Python版本的二进制兼容性得到了质的飞跃。你编译出的一个.so或.pyd文件有可能在多个Python小版本间无需重新编译即可使用极大地简化了打包和分发流程。其次模块初始化的安全性和性能更好。新的模块初始化API更健壮减少了因初始化顺序问题导致的崩溃。最后对新C标准的支持更完善让你能更自然地在绑定代码中使用C17甚至C20的特性。接下来我将从设计思路、核心特性拆解、实战迁移步骤到避坑指南为你完整呈现如何利用pybind11 v3.0突破兼容性瓶颈。2. 核心兼容性特性深度解析与设计思路pybind11 v3.0的许多改进都围绕着“如何与不断演进的Python C API和谐共处”这一核心命题。理解这些设计背后的逻辑能帮助你在遇到问题时更快地定位和解决。2.1 基于PEP 384的稳定ABI支持这是v3.0最重量级的特性之一。在Python 3.2之前每个Python版本都有自己的C API导致为Python 3.8编译的扩展模块无法在Python 3.9上加载。PEP 384引入了“稳定ABI”Stable ABI它定义了一组有限的、保证在Python 3.x系列中保持稳定的C API函数和结构体。pybind11 v3.0充分利用了这一点。它是如何工作的当你使用pybind11时你的代码最终会调用诸如PyLong_FromLong、PyList_New这样的Python C API函数。在非稳定ABI模式下这些函数是直接通过函数指针调用的这些指针的地址和签名可能随Python版本改变。而在稳定ABI模式下pybind11通过一个名为Py_LIMITED_API的宏和一组预定义的“桩函数”来访问API。这些桩函数在运行时通过一个查找表动态解析到当前Python解释器真正的实现上。实战配置在你的setup.py或CMakeLists.txt中启用稳定ABI支持是关键一步。以setuptools为例# setup.py from setuptools import setup, Extension import pybind11 ext_modules [ Extension( my_module, [src/main.cpp], include_dirs[pybind11.get_include()], languagec, # 定义Py_LIMITED_API宏并指定一个版本号例如0x03080000对应3.8 define_macros[(Py_LIMITED_API, 0x03080000)], # 通常需要链接Python库但稳定ABI下链接方式可能不同 # 具体取决于你的构建系统 ), ] setup( namemy_module, ext_modulesext_modules, # ... 其他参数 )注意启用Py_LIMITED_API意味着你只能使用PEP 384中列出的那部分“稳定”的C API。pybind11 v3.0已经帮我们处理了绝大部分转换但如果你在绑定代码中直接写了一些“不稳定”的C API调用比如直接操作PyObject的内部字段这些代码在启用稳定ABI后可能会编译失败或运行错误。因此尽量使用pybind11提供的封装接口。带来的好处二进制兼容性理论上用Py_LIMITED_API0x03080000Python 3.8编译的扩展可以在任何Python 3.8且未来某个保证ABI稳定的版本上运行。这为打包如制作manylinux轮子带来了极大便利。安全性减少了对Python内部结构的直接依赖使得扩展模块更不容易因为Python解释器的内部改动而崩溃。2.2 多阶段初始化PEP 489的集成Python 3.5的PEP 489引入了多阶段初始化旨在解决传统模块初始化的一些问题如模块状态管理混乱、子解释器支持差等。pybind11 v3.0对此提供了更优雅的支持。传统初始化 vs 多阶段初始化传统方式单阶段PyMODINIT_FUNC PyInit_my_module(void)函数一次性完成所有工作创建模块对象、填充方法表、添加类型、初始化模块级状态。如果失败清理工作很麻烦。多阶段方式将初始化分为多个阶段Py_mod_create,Py_mod_exec。Py_mod_create阶段只负责创建模块对象本身一个空壳Py_mod_exec阶段才执行填充模块内容等可能失败的操作。这样设计更清晰也支持在子解释器中更好地隔离模块状态。在pybind11 v3.0中的使用对于大多数用户你不需要显式处理多阶段初始化。pybind11的PYBIND11_MODULE宏在检测到Python版本支持PEP 489时会自动生成符合多阶段初始化的代码。你感受到的好处是隐性的模块导入更健壮尤其是在复杂的插件系统或嵌入场景中。但是如果你需要定义模块级状态module-level state多阶段初始化的优势就体现出来了。在v3.0中你可以更安全地做到这一点#include pybind11/pybind11.h namespace py pybind11; // 假设我们有一个需要跨函数共享的模块状态 struct ModuleState { int default_value 42; std::string config; }; // PYBIND11_MODULE宏内部会处理多阶段初始化的细节 PYBIND11_MODULE(my_module, m) { // 获取或创建模块状态在多阶段初始化下是安全的 // 注意具体API可能涉及py::module_::import和状态管理这里是一个概念示例 // pybind11提供了更高级的封装来管理模块状态避免直接操作低级API。 m.def(get_default, []() { // 如何安全地访问ModuleState在v3.0的实践中通常使用 // 1. 在pybind11内部静态变量对于简单情况。 // 2. 使用Python的capsule或模块字典存储状态。 // 3. 对于复杂的、需要隔离的状态应使用py::module_::add_object和特别的生命周期管理。 // 直接使用静态变量在多阶段初始化/子解释器中可能有问题。 static ModuleState state; // 谨慎使用在子解释器中这可能不是独立的。 return state.default_value; }); }实操心得除非你确实需要为模块维护复杂的、需要隔离的全局状态否则尽量不要自己手动管理模块状态。利用pybind11封装好的功能如将状态绑定到Python对象或模块属性上是更安全、更符合Python哲学的做法。多阶段初始化主要是为pybind11和Python解释器本身服务的它让底层基础设施更稳固。2.3 针对Python 3.8内部变更的适配Python 3.8、3.9、3.10等版本在内部进行了不少优化和清理其中一些影响了C API。pybind11 v3.0紧跟这些变化做了大量适配工作。Python 3.8: 向量调用协议Vectorcall这是一种新的、更高效的函数调用协议。pybind11 v3.0在绑定函数和类的方法时会在支持的情况下优先使用向量调用这能带来小幅的性能提升尤其是在大量调用小函数时。Python 3.9: 解释器状态管理API变更一些关于线程和解释器状态的内部API被调整或标记为过时。pybind11 v3.0更新了其内部使用的相关代码确保在新版本上编译不产生警告运行不依赖已废弃的API。Python 3.10: 更严格的C API弃用策略Python 3.10开始一些不安全的C API在默认情况下会触发编译警告。pybind11 v3.0的代码库已经过清理以减少甚至消除这些警告让你的构建输出更干净。对开发者的直接影响你不需要为了适配这些Python内部变更而去修改自己的绑定代码。pybind11 v3.0作为一个中间层已经帮你做好了兼容。你只需要升级pybind11并在支持新特性的Python版本上重新编译你的扩展就能自动获得这些改进如性能提升、更少的编译警告。3. 从旧版本迁移至v3.0的实战步骤将现有项目从pybind11 v2.x迁移到v3.0通常是一个平滑的过程但也有一些需要注意的细节。以下是一个循序渐进的迁移指南。3.1 环境准备与依赖检查备份你的项目这是任何迁移操作的第一步。确认Python版本确保你的开发环境和目标部署环境至少是Python 3.8。虽然pybind11 v3.0可能能在更早版本工作但为了充分利用新特性建议3.8。更新pybind11方法A推荐使用包管理器如果你通过pip install pybind11安装直接升级即可pip install --upgrade pybind11方法B作为子模块如果你的项目将pybind11作为git子模块git submodule引入更新子模块到v3.0.0或更高版本的tag。cd /path/to/your/project git submodule update --remote extern/pybind11 # 假设pybind11在extern/pybind11目录 cd extern/pybind11 git checkout v3.0.0方法C直接复制如果是直接复制头文件去官方GitHub仓库下载v3.0.0的发布包替换掉旧的include/pybind11目录。3.2 构建系统配置调整不同的构建系统需要不同的调整。对于CMake用户最常见 pybind11提供了一个优秀的CMake支持模块。迁移时重点检查CMakeLists.txt。cmake_minimum_required(VERSION 3.5...3.26) # v3.0建议CMake 3.53.26是当前较新版本 project(MyExample) # 1. 寻找pybind11包。新版pybind11Config.cmake更完善。 find_package(pybind11 3.0 REQUIRED CONFIG) # 明确要求3.0版本 # 如果find_package找不到而你用的是子模块可以用add_subdirectory # add_subdirectory(extern/pybind11) # 2. 添加你的扩展模块 pybind11_add_module(my_module src/main.cpp) # 3. 设置目标属性。v3.0后更推荐使用target_compile_features和target_link_libraries来管理C标准。 target_compile_features(my_module PRIVATE cxx_std_17) # 例如要求C17 target_link_libraries(my_module PRIVATE pybind11::module) # 4. 可选但推荐启用稳定ABI。这通常在find_package时通过选项设置或直接设置宏。 # 一种方法是通过pybind11的扩展选项 # set(PYBIND11_STABLE_ABI ON) # 在find_package或add_subdirectory之前设置 # 另一种是直接为目标添加定义 # target_compile_definitions(my_module PRIVATE Py_LIMITED_API0x03080000)关键调整点find_package中指定CONFIG模式并版本要求3.0。使用pybind11::module等现代CMake目标进行链接而不是旧的pybind11::pybind11虽然可能仍有效。考虑是否启用PYBIND11_STABLE_ABI。对于setuptools用户 主要调整setup.py。from setuptools import setup, Extension import pybind11 ext_modules [ Extension( my_module, [src/main.cpp], include_dirs[ pybind11.get_include(), # 这个函数能正确找到升级后的头文件路径 # 你的其他头文件路径... ], languagec, # 强烈建议指定C标准 extra_compile_args[-stdc17, -O3], # 可选启用稳定ABI define_macros[(Py_LIMITED_API, 0x03080000)], ), ] setup( namemy_module, ext_modulesext_modules, # 安装时确保pybind11可用通常作为setup_requires或install_requires setup_requires[pybind113.0.0], install_requires[pybind113.0.0], # 如果运行时也需要例如用于查找头文件路径 # ... 其他参数 )3.3 代码层面的适配与更新大部分pybind11 v2.x的代码在v3.0上无需修改即可编译运行。但有一些改进和清理点值得关注检查过时的APIpybind11 v3.0移除或清理了一些在v2.x中已弃用的API。编译时关注警告信息。常见的如py::class_的旧式构造函数注册方式如果用了编译器会报错需改用.def(py::init...())。一些旧的、功能重复的类型转换辅助函数。利用新特性可选例如如果你之前用复杂的方法处理std::variant或std::optional可以检查v3.0是否有更简洁的内置支持。查阅v3.0的Changelog了解是否有你感兴趣的新绑定辅助工具。重新编译与测试这是最关键的一步。在迁移后务必彻底重新编译你的扩展模块。不要复用旧的编译产物。编译完成后运行你完整的测试套件确保所有功能正常。4. 新特性实战构建一个兼容Python 3.8-3.12的C扩展让我们通过一个具体的例子将上述理论付诸实践。我们将创建一个简单的数学计算模块它包含一个函数和一个类并演示如何配置以支持从Python 3.8到3.12的多个版本。4.1 项目结构与核心C代码假设我们的项目结构如下my_math_module/ ├── CMakeLists.txt ├── setup.py (备用) └── src/ ├── my_math.cpp └── my_math.h (可选)src/my_math.cpp内容#include pybind11/pybind11.h #include pybind11/stl.h // 用于STL容器自动转换 #include vector #include numeric #include cmath namespace py pybind11; // 一个简单的函数计算向量元素的平方和 double sum_of_squares(const std::vectordouble vec) { return std::accumulate(vec.begin(), vec.end(), 0.0, [](double acc, double val) { return acc val * val; }); } // 一个简单的类代表一个二维点并能计算到原点的距离 class Point { public: Point(double x, double y) : x_(x), y_(y) {} double distance_to_origin() const { return std::sqrt(x_ * x_ y_ * y_); } void scale(double factor) { x_ * factor; y_ * factor; } double get_x() const { return x_; } double get_y() const { return y_; } void set_x(double x) { x_ x; } void set_y(double y) { y_ y; } private: double x_; double y_; }; // 使用PYBIND11_MODULE宏创建模块 PYBIND11_MODULE(my_math, m) { m.doc() A simple math module built with pybind11 v3.0; // 绑定函数 m.def(sum_of_squares, sum_of_squares, Calculate the sum of squares of a vector, py::arg(vec)); // 绑定类 py::class_Point(m, Point) .def(py::initdouble, double(), py::arg(x), py::arg(y)) .def_property(x, Point::get_x, Point::set_x) .def_property(y, Point::get_y, Point::set_y) .def(distance_to_origin, Point::distance_to_origin) .def(scale, Point::scale, py::arg(factor)) .def(__repr__, [](const Point p) { return Point( std::to_string(p.get_x()) , std::to_string(p.get_y()) ); }); }4.2 CMakeLists.txt配置详解这是支持跨版本兼容构建的核心。cmake_minimum_required(VERSION 3.5) project(my_math_module LANGUAGES CXX) # 设置C标准 - 使用现代C特性 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # 禁用编译器扩展保证可移植性 # 查找Python解释器和开发库 find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development) # 查找pybind11 (v3.0) find_package(pybind11 3.0 REQUIRED CONFIG) # 打印找到的版本信息用于确认 message(STATUS Found Python ${Python_VERSION}) message(STATUS Found pybind11 v${pybind11_VERSION}) # 可选启用稳定ABI构建。 # 这将定义一个Py_LIMITED_API宏并可能影响一些pybind11内部实现。 # 对于希望一个二进制文件在多个Python小版本上运行的用户可以打开此选项。 # 注意启用后某些高级或依赖不稳定Python内部API的特性可能受限。 # set(PYBIND11_STABLE_ABI ON) # 取消注释以启用 # 添加模块 pybind11_add_module(my_math src/my_math.cpp) # 设置输出目录方便测试可选 set_target_properties(my_math PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} ) # 更精细的目标属性设置 target_compile_features(my_math PRIVATE cxx_std_17) # 链接pybind11::module它包含了所有必要的依赖 target_link_libraries(my_math PRIVATE pybind11::module) # 安装规则可选用于打包 install(TARGETS my_math LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}/site-packages )关键配置解析find_package(Python ... Development)确保找到Python的头文件和库。Development组件是关键。find_package(pybind11 3.0 REQUIRED CONFIG)要求pybind11 v3.0并通过CONFIG模式查找通常由pybind11自己的CMake配置文件提供。set(PYBIND11_STABLE_ABI ON)这是一个全局开关。打开后pybind11会为所有通过pybind11_add_module添加的模块启用稳定ABI支持。你也可以通过target_compile_definitions为单个目标设置。pybind11_add_module这个宏简化了模块创建过程自动处理了模块命名、链接Python库等繁琐步骤。target_link_libraries(my_math PRIVATE pybind11::module)链接到pybind11::module目标它包含了正确的包含路径、编译定义和链接库。4.3 编译、测试与跨版本验证编译步骤mkdir build cd build # 如果你希望启用稳定ABI可以通过-D传递选项如果CMakeLists里用了set(PYBIND11_STABLE_ABI ON) # cmake .. -DPYBIND11_STABLE_ABION cmake .. cmake --build . --config Release编译后你会在build目录或Release子目录下找到my_math.cpython-XX-XX-XX.soLinux/macOS或my_math.pydWindows文件。基础功能测试创建一个简单的Python脚本test_my_math.pyimport sys import my_math print(fTesting my_math module built with pybind11 v3.0) print(fPython version: {sys.version}) # 测试函数 vec [1.0, 2.0, 3.0] result my_math.sum_of_squares(vec) print(fsum_of_squares({vec}) {result}) # 应输出 14.0 # 测试类 p my_math.Point(3.0, 4.0) print(fCreated {p}) print(fDistance to origin: {p.distance_to_origin()}) # 应输出 5.0 p.scale(2.0) print(fAfter scaling by 2: {p}) print(fNew distance: {p.distance_to_origin()}) # 应输出 10.0跨版本兼容性验证思路使用稳定ABI编译在CMake中启用PYBIND11_STABLE_ABI并指定一个较低的Py_LIMITED_API版本号如0x03080000。编译生成一个扩展模块。在不同Python环境中测试准备多个Python环境3.8, 3.9, 3.10, 3.11, 3.12可以使用conda或pyenv创建。将编译好的单个.so文件复制到这些环境的site-packages目录或直接放在脚本同级目录用sys.path引入。运行测试脚本在每个环境中运行上面的测试脚本。如果模块能成功导入并运行说明稳定ABI生效了。性能与功能对比你也可以对比启用和不启用稳定ABI时模块的性能如函数调用开销是否有可察觉的差异。对于大多数应用差异微乎其微。注意事项稳定ABI不是银弹。它限制了你只能使用Python C API的一个子集。如果你的扩展模块使用了某些“不稳定”的API比如通过pybind11的某些高级特性间接使用那么在启用稳定ABI后可能会失败。因此启用稳定ABI后必须进行全面的测试。对于绝大多数使用pybind11标准绑定功能的项目v3.0的稳定ABI支持是可靠可用的。5. 高级特性与性能优化技巧除了解决兼容性pybind11 v3.0也带来了一些能提升开发效率和运行性能的新特性或改进。5.1 更智能的类型转换与内存视图pybind11 v3.0进一步优化了C与Python之间数据交换的效率。py::array_t的增强对于科学计算numpy数组的互操作至关重要。v3.0对py::array_t的支持更完善特别是在处理非连续内存布局strided layout或复杂数据类型时转换更高效错误信息更友好。// 示例接受一个numpy数组并返回它的一个标量统计值避免拷贝 m.def(mean_of_array, [](py::array_tdouble arr) { auto buf arr.request(); // 获取缓冲区信息 double* ptr static_castdouble*(buf.ptr); size_t size buf.size; double sum 0.0; for(size_t i0; isize; i) { sum ptr[i]; } return sum / size; }, py::arg(array).noconvert()); // .noconvert()要求传入的就是array_t避免不必要的转换尝试py::buffer_protocol对于实现Python缓冲区协议的对象如memoryview,bytes, 自定义对象v3.0提供了更直接和高效的处理方式方便进行零拷贝数据交换。5.2 模块本地状态Module-local与子解释器支持这对于创建可重入的、线程安全的插件系统非常重要。pybind11 v3.0改进了模块状态的管理使其更好地与PEP 489的多阶段初始化协同工作。模块本地状态确保模块的全局变量在每个子解释器中是独立的。虽然通过静态变量也能模拟但v3.0提供了更规范的途径尽管大部分工作由宏在背后完成。这在你需要将Python嵌入到多线程的C应用中且每个线程运行独立的子解释器时能避免状态污染。使用建议除非你明确需要嵌入Python或使用子解释器否则这个特性你可能感知不到。但它的存在意味着你的模块在更复杂的环境下行为更可预测。5.3 编译期检查与错误提示优化v3.0增加了更多的静态断言static_assert和编译期检查能在编译阶段捕获更多的绑定错误而不是等到运行时才崩溃。例如在绑定一个不存在的类方法时编译器可能会给出更清晰的错误信息。同时运行时错误信息也更友好。当Python参数类型不匹配时产生的TypeError异常信息会尝试指出期望的类型和实际收到的类型这对于调试非常有用。6. 常见问题排查与避坑指南即使有了v3.0在实际使用中仍可能遇到一些问题。这里记录一些常见陷阱和解决方法。6.1 编译错误与链接问题问题现象可能原因解决方案fatal error: pybind11/pybind11.h file not found头文件路径未正确包含。确保find_package(pybind11)成功并将pybind11::headers或pybind11::module添加到目标的include_directories或target_link_libraries中。在setup.py中使用pybind11.get_include()。undefined reference toPyInit_my_module‘链接时未链接Python库。使用pybind11_add_module或正确链接pybind11::module目标它们会自动处理Python库的链接。手动编写链接指令时确保链接Python::PythonCMake或python3.x库。启用Py_LIMITED_API后编译失败提示某些API未定义你的代码或pybind11内部使用了不在稳定ABI列表中的Python C API。1. 确保你使用的pybind11版本3.0且支持稳定ABI。2. 检查你的绑定代码是否直接调用了不稳定的C API。尽量使用pybind11的封装。3. 可能是pybind11的某个特性还不完全支持稳定ABI查阅其issue或文档。C标准不匹配导致的语法错误你的C代码使用了C17/20特性但编译器未启用相应标准。在CMake中设置set(CMAKE_CXX_STANDARD 17)和target_compile_features(my_module PRIVATE cxx_std_17)。在setup.py的extra_compile_args中添加-stdc17。6.2 运行时导入与执行错误问题现象可能原因解决方案ImportError: dynamic module does not define module export function (PyInit_my_module)模块初始化函数名不匹配。PYBIND11_MODULE宏的第一个参数模块名必须与Extension构造函数中的模块名、以及生成的二进制文件名核心部分一致。检查三者是否完全一致大小写敏感。例如PYBIND11_MODULE(my_math, m)则扩展名应为my_math生成的库文件应为my_math.cpython-...so。ImportError: /path/to/module.so: undefined symbol: _PyArg_ParseStack通常是因为用高版本Python编译的扩展在低版本Python中加载或者稳定ABI配置有问题。1. 确保编译环境和运行环境的Python主版本一致如都是3.x。2. 如果使用了稳定ABI确保Py_LIMITED_API的版本号不高于运行环境的Python版本。程序在导入模块时崩溃Segmentation Fault模块初始化顺序问题、全局静态变量初始化顺序问题、或访问了已释放的Python对象。1. 使用PEP 489多阶段初始化pybind11 v3.0默认处理有助于缓解初始化问题。2. 避免在C全局静态变量的构造函数或析构函数中进行复杂的pybind11操作或调用Python API。3. 使用py::gil_scoped_acquire确保在从非Python线程调用Python API时持有GIL。性能不如预期函数调用开销、数据拷贝过多。1. 对于频繁调用的小函数考虑使用py::call_guardpy::gil_scoped_release()在函数执行期间释放GIL前提是函数不调用Python API且线程安全。2. 使用py::array_t或缓冲区协议进行大数据传递避免逐元素转换。3. 使用C侧的类型如std::vector而非频繁在Python容器和C容器间转换。6.3 调试技巧使用调试符号编译在CMake的build类型中选择Debug或在setup.py的extra_compile_args中添加-g。这样当崩溃时gdb或lldb能给出更详细的堆栈信息。在Python中捕获C异常pybind11会自动将C标准异常转换为Python异常。确保你的C代码抛出的异常是std::exception的子类这样你就能在Python中用try...except捕获到有意义的错误信息。使用pybind11::print在C代码中可以使用pybind11::print(...)来打印调试信息它会输出到Python的sys.stdout比直接用std::cout更可靠尤其是在嵌入环境中。迁移到pybind11 v3.0并充分利用其新特性是一个提升项目健壮性和面向未来兼容性的明智投资。整个过程的核心在于理解稳定ABI和多阶段初始化如何为你保驾护航并仔细调整你的构建配置。当你成功编译出一个能在多个Python版本上平滑运行的扩展时那种解脱感和成就感会让你觉得之前所有的折腾都是值得的。