pybind11 的核心用法是通过 PYBIND11_MODULE 宏创建一个 Python 扩展模块,然后使用 m.def() 暴露函数,py::class_ 暴露类,并借助头文件实现 STL 容器的自动转换。下面从基础到进阶,系统介绍如何暴露 C++ 接口。
📦 基础框架:创建 Python 模块
所有 pybind11 绑定都从 PYBIND11_MODULE 宏开始。它定义了模块名,并提供一个 py::module_ 对象(通常命名为 m),后续所有绑定操作都通过它进行。
cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example module"; // 模块的文档字符串
// 在此处暴露函数、类等
}
编译后,Python 中即可通过 import example 使用。
🧩 暴露普通函数
使用 m.def() 将 C++ 函数(或 Lambda)绑定到 Python 模块。
cpp
int add(int a, int b) { return a + b; }
PYBIND11_MODULE(example, m) {
m.def("add", &add, "A function that adds two numbers");
// Lambda 同样支持
m.def("multiply", [](double x, double y) { return x * y; });
}
命名参数与默认值
通过 py::arg() 指定参数名,并可用 = 设置默认值,使 Python 端支持关键字调用。
cpp
m.def("add", &add, py::arg("a"), py::arg("b") = 10);
// Python 中可写 add(a=5) 或 add(5),b 默认为 10
函数重载
pybind11 会自动处理 C++ 重载函数,只需将不同的函数指针分别 def 到同一个 Python 名称即可,调用时会按参数类型自动匹配。
cpp
void process(int x) { /* ... */ }
void process(const std::string& s) { /* ... */ }
m.def("process", py::overload_cast<int>(&process));
m.def("process", py::overload_cast<const std::string&>(&process));
🏗️ 暴露类与结构体
py::class_ 是暴露自定义类型的核心,通过链式 .def() 绑定构造函数、方法和属性。
cpp
struct Pet {
Pet(const std::string &name) : name(name) {}
void setName(const std::string &name_) { name = name_; }
const std::string &getName() const { return name; }
std::string name;
};
PYBIND11_MODULE(example, m) {
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string &>()) // 构造函数
.def("setName", &Pet::setName) // 方法
.def("getName", &Pet::getName)
.def_readwrite("name", &Pet::name); // 可读写属性
}
py::init<...>():绑定构造函数,模板参数即构造函数的参数类型。def_readwrite/def_readonly:直接暴露 public 成员变量为 Python 属性。
对于私有构造函数或工厂模式,可用 py::init() 配合 Lambda 返回对象实例。
🔄 STL 容器的自动转换
只需额外包含头文件 <pybind11/stl.h>,pybind11 便会自动在以下 C++ 类型与 Python 内置容器之间进行转换:
| C++ 类型 | Python 类型 |
|---|---|
std::vector, std::list, std::deque, std::array |
list |
std::set, std::unordered_set |
set |
std::map, std::unordered_map |
dict |
std::pair, std::tuple |
tuple |
cpp
#include <pybind11/stl.h>
std::vector<int> double_elements(const std::vector<int>& v) {
std::vector<int> result;
for (int x : v) result.push_back(x * 2);
return result;
}
m.def("double_elements", &double_elements);
// Python: example.double_elements([1,2,3]) → [2,4,6]
⚠️ 注意 :自动转换涉及拷贝 ,会丧失 C++ 的引用语义,每次跨界都会复制整个容器,对大型数据有性能影响。若需零拷贝或引用传递,可考虑使用 pybind11/stl_bind.h 进行不透明绑定,或改用 NumPy 数组。
🧬 继承与多态
pybind11 能自动识别 C++ 的 virtual 函数,实现多态对象的自动向上转型(upcasting)。绑定派生类时,将基类作为 py::class_ 的第二个模板参数传入。
cpp
struct PolymorphicPet {
virtual ~PolymorphicPet() = default;
virtual std::string speak() const { return "..."; }
};
struct Dog : PolymorphicPet {
std::string speak() const override { return "Woof!"; }
std::string fetch() const { return "Ball"; }
};
py::class_<PolymorphicPet>(m, "PolymorphicPet")
.def("speak", &PolymorphicPet::speak);
py::class_<Dog, PolymorphicPet>(m, "Dog")
.def(py::init<>())
.def("fetch", &Dog::fetch);
// 返回基类指针时,若实际对象是 Dog,Python 端会得到 Dog 类型
m.def("make_pet", []() -> std::unique_ptr<PolymorphicPet> {
return std::make_unique<Dog>();
});
在 Python 中,make_pet() 返回的对象类型为 Dog,可直接调用 fetch(),尽管 C++ 返回的是 PolymorphicPet*。这超越了标准 C++ 的虚函数机制,pybind11 会恢复完整的派生类型信息。
⚠️ 异常处理
pybind11 内置了 C++ 标准异常到 Python 异常的自动翻译,无需手动捕获。
| C++ 异常 | Python 异常 |
|---|---|
std::invalid_argument |
ValueError |
std::out_of_range |
IndexError |
std::bad_alloc |
MemoryError |
std::exception(兜底) |
RuntimeError |
cpp
void validate(int x) {
if (x < 0) throw std::invalid_argument("x must be non-negative");
}
m.def("validate", &validate);
// Python: example.validate(-1) → ValueError: x must be non-negative
自定义异常 :若需要将特定的 C++ 异常类映射为自定义 Python 异常,可使用 py::register_exception。
cpp
py::register_exception<MyCppException>(m, "MyError", PyExc_RuntimeError);
此后从 C++ 抛出的 MyCppException 会在 Python 端以 MyError 的形式出现。
💡 补充要点
- 智能指针 :pybind11 原生支持
std::shared_ptr和std::unique_ptr,返回智能指针时,Python 对象会自动管理其生命周期。pybind11 v3 起推荐使用py::smart_holder以获得更安全的默认行为。 - NumPy 零拷贝 :包含
<pybind11/numpy.h>后,py::array_t<T>可与 NumPy 数组零拷贝交互,适合暴露大规模数值计算接口。 - 枚举 :使用
py::enum_将 C++enum暴露给 Python,支持位运算和值转换。 - GIL 管理 :在 C++ 中调用 Python 对象(如
py::object)时必须持有 GIL(全局解释器锁),pybind11 在大多数入口处会自动处理,但在纯 C++ 线程中回调 Python 需手动py::gil_scoped_acquire。
暴露 C++ 接口的核心路径可以归纳为:m.def 绑函数 → py::class_ 绑类 → 按需引入 <pybind11/stl.h> 等头文件启用自动转换 → 用继承绑定实现多态 → 异常自动翻译。从最简单的自由函数开始,逐步扩展到类、容器和多态,即可覆盖绝大多数工程场景。
以上由 deepseek 帮忙整理。