Skip to content

简易实现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_propertydef_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_ptrstd::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的集成变得更加简洁和高效,极大地提升了开发效率。

Released under the BSD3 License