简易实现class绑定
本章将进一步讲解并演示一个通过PyBind11实现C++类绑定的完整示例。
基础类绑定
以下是一个简单的C++类绑定示例,展示了如何将C++类及其方法暴露给Python。
1. 定义C++类
一个简单的C++类Pet
,它有一个名称属性和一个发声方法:
cpp
#include <iostream>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
class Pet
{
private:
std::string mName;
public:
Pet(const std::string& name) : mName(name) {}
void setName(const std::string& name) { mName = name; }
const std::string& getName() const { return mName; }
void speak() const { std::cout << mName << " 说: 喵喵喵!\n"; }
};
2. 绑定类到Python
使用PyBind11的py::class_
模板将C++类绑定到Python模块中:
cpp
PYBIND11_MODULE(testLib, m)
{
m.doc() = "这是一个绑定测试库";
// 绑定Pet类
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string&>()) // 绑定构造函数
.def("setName", &Pet::setName) // 绑定setName方法
.def("getName", &Pet::getName) // 绑定getName方法
.def("speak", &Pet::speak); // 绑定speak方法
}
3. 使用绑定的类
编译生成的Python模块后,可以在Python中使用绑定的Pet
类:
python
import testLib
myPet = testLib.Pet("小猫")
myPet.speak() # 输出: 小猫 说: 喵喵喵!
myPet.setName("小狗")
print(myPet.getName()) # 输出: 小狗
自动属性绑定
PyBind11还支持绑定类的属性,使其可以像Python属性一样访问。以下是一个示例:
cpp
#include <iostream>
#include <pybind11/pybind11.h>
namespace py = pybind11;
class Vec3
{
public:
float x, y, z;
Vec3(): x(0.0f), y(0.0f), z(0.0f) {}
Vec3(float x = 0.0f, float y = 0.0f, float z = 0.0f) : x(x), y(y), z(z) {}
};
PYBIND11_MODULE(testLib, m)
{
py::class_<Vec3>(m, "Vec3")
.def(py::init<>()) // 默认构造函数
.def(py::init<float, float, float>(), py::arg("x")=0.0f, py::arg("y")=0.0f, py::arg("z")=0.0f) // 带默认参数的构造函数(重载方法)
.def_readwrite("x", &Vec3::x) // 绑定读写属性(自动生成getter/setter函数)
.def_readwrite("y", &Vec3::y)
.def_readwrite("z", &Vec3::z);
// .def_property_readonly("...", &xxx) // 可选只读属性
}
property绑定
PyBind11允许你通过def_property
和def_property_readonly
方法绑定属性。以下是一个示例:
cpp
PYBIND11_MODULE(testLib, m)
{
py::class_<Vec3>(m, "Vec3")
// .def(...)
// ...
.def_property("magnitude",
[](const Vec3 &v) { return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z); }, // getter
nullptr, // setter (只读属性可设为nullptr)
"计算向量的大小"
);
}
智能指针支持
PyBind11支持使用智能指针(如std::shared_ptr
和std::unique_ptr
)来管理C++对象的生命周期。以下是一个示例:
cpp
#include <iostream>
#include <memory>
#include <pybind11/pybind11.h>
namespace py = pybind11;
class Resource
{
public:
// 输出日志,便于观察C++对象的生命周期
Resource() { std::cout << "Resource created\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
void greet() { std::cout << "Hello from Resource\n"; }
};
class Manager
{
private:
std::shared_ptr<Resource> resource;
public:
Manager() : resource(std::make_shared<Resource>()) {}
// 返回智能指针,Python层获得共享所有权
std::shared_ptr<Resource> getResource()
{
return resource;
}
};
PYBIND11_MODULE(testLib, m)
{
// 使用引用计数器策略绑定
py::class_<Resource, std::shared_ptr<Resource>>(m, "Resource")
.def("greet", &Resource::greet);
py::class_<Manager>(m, "Manager")
.def(py::init<>())
.def("getResource", &Manager::getResource);
}
Python测试:
python
import testLib
mgr = testLib.Manager()
res = mgr.getResource() # 获取资源(引用计数智能管理)
res.greet() # 调用Resource的方法
del mgr # 删除Manager引用(对象引用归0被销毁,但Resource仍被Py引用)
res1.greet() # 由于共享引用计数此时还能正常调用
del res # 删除Python引用,若无其他引用则C++对象会被销毁
同理,也可以使用std::unique_ptr
,但需要注意其独占所有权的特性。
多态转发
PyBind11支持C++的多态特性,可以绑定基类和派生类
问题示例
cpp
#include <iostream>
#include <pybind11/pybind11.h>
namespace py = pybind11;
class NativeEntity
{
public:
virtual ~NativeEntity() = default;
virtual void update() { std::cout << "NativeEntity update\n"; }
};
static void callEntityUpdate(NativeEntity& entity)
{
// 调用虚函数update
entity.update();
}
PYBIND11_MODULE(testLib, m)
{
py::class_<NativeEntity>(m, "NativeEntity")
.def(py::init<>())
.def("update", &NativeEntity::update);
m.def("callEntityUpdate", &callEntityUpdate, "调用实体的update方法");
}
Python测试:
python
from testLib import NativeEntity, callEntityUpdate
class Zombie(NativeEntity):
def update(self):
print("Zombie update")
# Python重写了update并直接由Python调用,可以正常工作
z = Zombie()
z.update() # √ 正常输出: Zombie update
# 但通过C++调用时,仍然调用的是基类的update方法
callEntityUpdate(z) # × 错误输出: NativeEntity update (C++测并不知道Python重写的方法)
使用跳板类解决
为了解决上述问题,需要创建一个跳板类(trampoline class),它继承自基类并重写虚函数,以便在C++层正确调用Python重写的方法。
cpp
// 为什么不直接在NativeEntity中实现?因为有时我们无法修改已有的C++类,这会破坏封装,
// trampoline类是pybind11设计的推荐方式,避免修改已有C++代码,特别是第三方库时十分重要。
class PyNativeEntity : public NativeEntity
{
public:
using NativeEntity::NativeEntity; // 继承构造函数
// 重写虚函数,使用PYBIND11_OVERRIDE宏调用Python的重写版本
void update() override
{
/*
该宏会自动处理Python对象update的行为:
自动转发给NativeEntity/重写+super转发/直接覆写
*/
PYBIND11_OVERRIDE(
void, // 返回类型
NativeEntity, // 基类
update, // 函数名
/* 无参数 */ // 参数列表
);
}
};
PYBIND11_MODULE(testLib, m)
{
// 绑定特殊实现的跳板类 <基类, 跳板类>
py::class_<NativeEntity, PyNativeEntity>(m, "NativeEntity")
.def(py::init<>())
.def("update", &PyNativeEntity::update);
m.def("callEntityUpdate", &callEntityUpdate, "调用实体的update方法");
}
再次测试:
python
from testLib import NativeEntity, callEntityUpdate
class Zombie(NativeEntity):
def update(self):
print("Zombie update")
z = Zombie()
# Python直接调用 正常输出: Zombie update
z.update()
# C++调用 现在也能正确输出: Zombie update
callEntityUpdate(z)
总结
通过以上示例,我们展示了如何使用PyBind11绑定C++类及其方法、属性,并处理智能指针和多态等复杂场景。PyBind11强大的功能使得C++与Python的集成变得更加简洁和高效,极大地提升了开发效率。