Skip to content

第1章 认识Pybind11

Pybind11 是现代 C++ 与 Python 之间的一座桥梁。理解它的本质,是后续所有章节的基石。

Pybind11 是一个仅含头文件的 C++ 库(header-only library),它让你能够在 C++ 代码中编写 Python 绑定——无需借助额外的代码生成工具,也无需编写晦涩的接口描述文件。

#include <pybind11/pybind11.h>
PYBIND11_MODULE(example, m) {
m.def("add", [](int a, int b) { return a + b; });
}
import example
print(example.add(1, 2)) # 3

仅仅这几行代码,一个 C++ 函数就能在 Python 中直接调用。Pybind11 在编译时将 C++ 函数导出为 Python 可调用的对象,运行时 Python 解释器通过动态库加载这个模块,就像加载任何其他 Python 扩展一样。

  • C++11 及以上:Pybind11 大量使用模板元编程、移动语义、变参模板等现代 C++ 特性,因此要求编译器支持 C++11。推荐使用 C++14 或 C++17 以获得更完整的特性支持。
  • Python 2.7 或 Python 3.x:Pybind11 兼容 Python 2.7(有限支持)和 Python 3.6+,现代项目一律使用 Python 3。
  • 头文件依赖:所有功能都在 pybind11/ 目录下的头文件中实现,只需将这个目录加入 include 路径即可。

Pybind11 内部封装了 Python/C API。当你写 m.def("add", ...) 时,Pybind11 帮你完成以下工作:

  1. 创建一个 Python capsule 对象,持有 C++ 函数的指针
  2. 将该 capsule 注册到模块的字典中,使其对 Python 可見
  3. 处理 Python 对象与 C++ 类型之间的转换(argparse、kwarg 处理)

这些细节对使用者是透明的,你只需专注于写 C++ 代码。

选择绑定方案时,需要在学习曲线、性能、功能、维护成本之间做出权衡。以下是主流方案的横向对比。

特性Pybind11CythonctypesSWIG
语言风格纯 C++独有种姓语法Python 标准库接口描述文件
学习曲线中等(C++ 基础)中等(Cython 语法)低(纯 Python)高(IDL + 多语言)
运行性能极高极高中等(FFI 开销)中等
类型安全编译期检查编译期检查运行时检查生成代码质量参差
STL 容器支持原生需手动需手动部分支持
NumPy 集成优秀优秀一般一般
多语言绑定仅 Python仅 C/C++多语言多语言
维护成本低(无代码生成)中等低高(接口文件)
调试难度高(混合语言)中等低高(生成代码)
典型用户scikit-learn, pycocotoolsCython 自身, lxml标准库内置BLAS, GMP

Pybind11 适合那些已经有一定规模 C++ 代码库、想在 Python 中复用的开发者。它的优势在于:用 C++ 思维写 C++ 代码,不需要学习新的领域特定语言(DSL)。当你的 C++ 类已经在生产环境中经过验证,Pybind11 能以最小的摩擦暴露给 Python。

Cython 适合那些需要极致性能且愿意使用特殊语法的场景。Cython 编译器将类似 Python 的语法转译为 C,再编译为机器码,性能可以接近纯 C。代价是你需要学习 Cython 的类型声明语法,且生成的代码可读性差,调试困难。Cython 在科学计算生态中应用极广(NumPy 早期就是用 Cython 写的原型)。

ctypes 是 Python 标准库的一部分,零额外依赖,但只能调用 C 函数,不能直接暴露 C++ 类。你需要手动编写 C 接口层将 C++ 类”扁平化”为 C 函数,再通过 ctypes 调用。对于简单的 C 动态库封装,ctypes 足够用;但对于复杂的 C++ 类层次结构,维护成本会急剧上升。

SWIG 是一个通用的代码生成工具,可以通过一份接口文件同时生成 Python、Ruby、Perl 等多种语言的绑定。它的缺点在于:生成的代码质量参差,调试困难,接口文件的语法复杂,当 C++ 类层次复杂时 SWIG 常常生成无法编译的代码。

:::note 选择建议 已有 C++ 代码库 → Pybind11
性能至上且愿意学新语法 → Cython
简单 C 库封装 → ctypes
需要多语言绑定 → SWIG :::

Pybind11 的设计哲学是:让 C++ 代码自然地映射为 Python 对象,而不引入额外的概念负担。以下是它的核心特性。

Pybind11 自动处理 Python 对象与 C++ 基础类型之间的转换。你不需要手动注册转换器——从 int 到 py::int_,从 std::string 到 str,转换逻辑已经内置。

m.def("process", [](int n, double d, const std::string& s) {
// Python int -> C++ int
// Python float -> C++ double
// Python str -> C++ std::string
return "processed: " + s;
});
result = process(42, 3.14, "hello") # 类型自动转换

只需要几行代码,就能将 C++ 类暴露为 Python 类——包括构造函数、成员函数、属性、继承关系。

namespace py = pybind11;
class Animal {
public:
virtual ~Animal() = default;
virtual std::string speak() const = 0;
};
class Dog : public Animal {
public:
std::string speak() const override { return "Woof!"; }
};
PYBIND11_MODULE(animals, m) {
py::class_<Animal>(m, "Animal")
.def("speak", &Animal::speak);
py::class_<Dog, Animal>(m, "Dog")
.def(py::init<>())
.def("speak", &Dog::speak);
}

Python 端代码完全符合直觉:

dog = Dog()
print(dog.speak()) # "Woof!"

Pybind11 可以根据参数类型自动选择正确的重载版本。你只需要声明多个签名,Pybind11 会在 Python 调用时进行类型匹配。

m.def("add",
static_cast<int(*)(int, int)>(&add),
py::arg("a"), py::arg("b"));
m.def("add",
static_cast<std::string(*)(const std::string&, const std::string&)>(&add),
py::arg("s1"), py::arg("s2"));
print(add(1, 2)) # int 版本
print(add("a", "b")) # string 版本

C++ 异常会自动转换为 Python 异常。你可以在 C++ 代码中 throw 任何异常类型,Pybind11 会捕获它并将其映射为对应的 Python 异常类。

m.def("divide", [](double a, double b) {
if (b == 0) throw std::runtime_error("division by zero");
return a / b;
});
try:
divide(1, 0)
except RuntimeError as e:
print(e) # "division by zero"

std::vector、std::map、std::list 等 STL 容器可以直接与 Python 的 list、dict 进行双向转换,无需额外配置。

m.def("sum_vector", [](const std::vector<int>& v) {
return std::accumulate(v.begin(), v.end(), 0);
});
print(sum_vector([1, 2, 3, 4, 5])) # 15

Python 的 __doc__ 属性在 C++ 端直接定义,生成的模块自带文档。

m.doc() = "example module - demonstration of pybind11 features";
m.def("add", ... , "Adds two numbers together");

Pybind11 生成的模块和类会自动携带你在 C++ 端编写的文档字符串,在 Python 的 help() 和 IDE 悬停提示中都能看到。

Pybind11 不是银弹——理解它的适用场景,能让你在正确的场合选择它,在错误的场合避免它。

1. 已有成熟的 C++ 代码库

当你有一批经过测试的 C++ 算法、核心逻辑或数据结构,希望在 Python 数据科学生态中复用——Pybind11 是最自然的选择。scikit-learn 的底层就是用 C++ 编写并通过 Cython(而非 Pybind11,但思路类似)暴露给 Python 的。你的场景可能比 scikit-learn 简单得多,此时 Pybind11 比 Cython 门槛更低。

2. 性能瓶颈在 Python 端

Python 的 GIL 使得 CPU 密集型任务无法真正并行。解决这个问题的最直接办法是用 C++ 重写热点函数,通过 Pybind11 暴露给 Python。一个典型场景:图像处理流水线中,OpenCV 的核心算法已经用 C++ 优化过,你只需要写一层薄薄的绑定。

3. 需要暴露复杂的 C++ 类层次结构

如果你要将一个包含继承关系、模板类、多重继承的 C++ 类层次结构暴露给 Python,Pybind11 的表现远优于 ctypes。Pybind11 对多态、虚函数、shared_from_this 等模式有完整支持。

4. 团队中 C++ 开发者比 Python 开发者更擅长底层优化

有时 Python 团队并不擅长 C++ 优化,但公司有现成的 C++ 优化专家。此时 Pybind11 允许 C++ 专家在 C++ 端做性能优化,Python 开发者只需调用封装好的模块。

1. 纯 Python 项目,引入 C++ 毫无必要

如果你的代码在 Python 中运行良好,性能也足够,不要为了”看起来更专业”而引入 C++。C++ 带来的编译复杂性和调试难度是真实的代价。

2. 简单数值计算可以由 NumPy 向量化替代

如果性能瓶颈在于 Python 的 for 循环,首先尝试用 NumPy 向量化。NumPy 的底层已经是 C/Fortran,用 Pybind11 重新实现不会有任何优势。只有当你的算法无法向量化为止,Pybind11 才有意义。

3. 需要同时支持多语言绑定

如果你的底层库需要同时暴露给 Python、Ruby、R 等多种语言,SWIG 的一次生成多语言能力仍然有价值。Pybind11 只支持 Python,换语言意味着重写。

4. 团队缺乏 C++ 能力

Pybind11 绑定的调试需要同时理解 Python 和 C++ 两端的执行模型。如果团队中没有人能读懂编译错误信息或使用 GDB/LLDB 调试混合语言代码,维护成本会非常高。

:::caution 性能误区 很多开发者以为”用 C++ 实现就能加速”。这是一个常见的误解。Pybind11 的绑定层本身有开销——每次 Python 调用 C++ 函数,都涉及参数的类型转换和引用计数管理。如果你的函数只是做简单的加减乘除,Pybind11 的开销可能反而比纯 Python 慢。真正的性能收益来自于:C++ 端的计算量远大于类型转换开销。切忌过早优化,永远先用 Python 原型验证算法,再在瓶颈处用 C++ 加速。 :::

理解 Pybind11 的发展脉络,有助于理解它的设计决策和当前能力边界。

年份版本主要变化
20141.0Wenzel Jakob 首次发布,定位为”nanobind 的前身”
2015-20161.x稳定版发布,STL 容器支持逐步完善
20172.0C++11 完全支持,函数重载系统重构,exception 重映射
20182.2py::init 替代旧版构造注册方式,模块生命周期管理改进
20192.3Windows MSVC 支持显著改善,Python 2.7 进入维护模式
20202.4改进 type hint 支持,添加 PYBIND11_MODULE 宏
20212.5更完善的 const-correctness,vector 和 map 的隐式转换优化
20222.6默认不启用 std::unordered_map 的不安全隐式转换
20232.7-2.9C++17 if constexpr 简化实现,Python 3.12 支持,async 支持改进
2024-20253.xnanobind 成为官方推荐的新一代方案,但 Pybind11 仍在活跃维护

Pybind11 始终拒绝引入额外的代码生成步骤。与 SWIG 或 Cython 不同,Pybind11 不需要你运行一个预处理器来”生成”绑定代码。你只需要 #include <pybind11/...>,编译器在编译时完成所有工作。这个设计决策带来两大优势:

  1. 零构建依赖:不需要安装额外的代码生成工具,CMake 本身已经足够
  2. 类型安全:所有类型检查在编译期完成,C++ 编译器的报错比 SWIG 的运行时报错更有用

2019 年之后,社区出现了一个新项目 nanobind,由 Pybind11 同一作者主导开发。nanobind 专注于更小的二进制体积和更快的导入速度,但它需要代码生成(通过 nanobind --opt 工具),且生态不如 Pybind11 成熟。

目前社区的主流选择仍然是 Pybind11——它有更多的社区积累、更多的 Stack Overflow 答案、更稳定的第三方库支持。本书聚焦 Pybind11,但在需要时会标注 nanobind 可能更优的场景。

:::note 版本选择建议 生产环境请使用 Pybind11 2.9.x 或 3.x 最新稳定版。本书代码以 Pybind11 2.9 为基准,Python 要求 3.8+,C++ 要求 C++14 及以上。 :::