Python `pybind11`:C++ 库到 Python 的现代绑定工具

好的,各位观众老爷们,今天咱们来聊聊Python和C++的那些不得不说的事儿,哦不,是不得不“绑”的事儿——Pybind11!

想象一下,你用C++辛辛苦苦写了一个高性能的库,结果Python这个小妖精就是用不上,性能差距让人泪奔。怎么办?难道要放弃Python的简洁和生态吗?No way! Pybind11就是来拯救你的!

什么是Pybind11?

简单来说,Pybind11就是一个C++库,它能让你轻松地将C++代码暴露给Python,让Python可以像调用自己的亲儿子一样调用你的C++函数和类。它是一个header-only的库,不需要编译安装,直接include就能用,简直是懒人福音。

为什么要用Pybind11?

  • 高性能: C++的性能优势不用多说,对于计算密集型任务,用C++实现,然后用Pybind11暴露给Python,可以显著提高效率。
  • 代码复用: 已经有的C++代码,不想用Python重写?Pybind11可以让你直接用起来。
  • Python生态: Python拥有庞大的生态系统,各种库和工具应有尽有。用Pybind11可以将C++代码融入Python生态,方便使用。
  • 易用性: 相比于其他绑定工具(比如SWIG),Pybind11的设计更加现代化,API更加简洁易懂。

Pybind11的原理

Pybind11主要通过C++的模板元编程来实现绑定。它利用C++的类型推导能力,自动将C++类型转换为Python类型,反之亦然。这样,你就可以在Python中像操作Python对象一样操作C++对象,而不用关心底层的类型转换细节。

安装Pybind11

Pybind11是一个header-only的库,所以不需要编译安装。你只需要下载Pybind11的源代码,然后将include目录添加到你的编译器include路径中即可。通常使用包管理器安装更方便。

  • conda: conda install -c conda-forge pybind11
  • pip: pip install pybind11

Pybind11的简单例子

咱们先来一个最简单的例子,让Python调用一个C++函数:

C++代码 (example.cpp):

#include <pybind11/pybind11.h>

namespace py = pybind11;

int add(int i, int j) {
    return i + j;
}

PYBIND11_MODULE(example, m) {
    m.doc() = "pybind11 example plugin"; // 可选的模块文档

    m.def("add", &add, "A function which adds two numbers");
}

解释:

  • #include <pybind11/pybind11.h>: 引入Pybind11的头文件。
  • namespace py = pybind11;: 为了方便使用,我们通常会创建一个命名空间别名。
  • PYBIND11_MODULE(example, m): 这是一个宏,用于定义Python模块。example是模块的名字,m是一个py::module对象,用于定义模块中的函数和类。
  • m.doc() = "pybind11 example plugin";: 设置模块的文档字符串,这个字符串会在Python中使用help(example)时显示。
  • m.def("add", &add, "A function which adds two numbers");: 将C++函数add暴露给Python。"add"是Python中使用的函数名,&add是C++函数的指针,"A function which adds two numbers"是函数的文档字符串。

编译C++代码:

你需要使用一个C++编译器来编译这段代码,生成一个Python扩展模块。编译命令会因操作系统和编译器而异。以下是一些常见的例子:

Linux/macOS (g++):

g++ -O3 -Wall -shared -std=c++17 -fPIC $(python3 -m pybind11 --includes) example.cpp -o example.so

Windows (MSVC):

cl /std:c++17 /EHsc /W3 /LD example.cpp /I %PYTHON_HOME%include /I %PYTHON_HOME%includepybind11 /link /OUT:example.pyd

解释:

  • -O3: 开启最高级别的优化。
  • -Wall: 开启所有警告。
  • -shared: 生成共享库(Python扩展模块)。
  • -std=c++17: 使用C++17标准。
  • -fPIC: 生成位置无关代码(Position Independent Code)。
  • $(python3 -m pybind11 --includes): 获取Pybind11的头文件路径。
  • example.cpp: C++源代码文件。
  • -o example.so (Linux/macOS) / /OUT:example.pyd (Windows): 输出文件名。

Python代码:

import example

print(example.add(1, 2))  # 输出: 3
print(example.__doc__)    # 输出: pybind11 example plugin
print(example.add.__doc__)# 输出: A function which adds two numbers

搞定!

你现在可以在Python中调用C++的add函数了!

绑定C++类

光有函数还不够,咱们再来看看如何绑定C++类:

C++代码 (example.cpp):

#include <pybind11/pybind11.h>

namespace py = pybind11;

class Pet {
public:
    Pet(const std::string &name) : name_(name) {}
    void setName(const std::string &name_) { this->name_ = name_; }
    const std::string &getName() const { return name_; }

private:
    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("__repr__",
             [](const Pet &a) {
                 return "<example.Pet named '" + a.getName() + "'>";
             });
}

解释:

  • py::class_<Pet>(m, "Pet"): 创建一个Python类Pet,它对应C++类Pet
  • .def(py::init<const std::string &>()): 定义构造函数,接受一个字符串参数。
  • .def("setName", &Pet::setName): 将C++成员函数setName暴露给Python。
  • .def("getName", &Pet::getName): 将C++成员函数getName暴露给Python。
  • .def("__repr__", ...): 重载__repr__方法,这样在Python中打印Pet对象时,会显示更友好的信息。

编译C++代码 (同上)

Python代码:

import example

pet = example.Pet("Muffin")
print(pet.getName())  # 输出: Muffin
pet.setName("Rover")
print(pet.getName())  # 输出: Rover
print(pet)            # 输出: <example.Pet named 'Rover'>

绑定属性 (Properties)

如果你想让Python直接访问C++类的成员变量,可以使用property

C++代码 (example.cpp):

#include <pybind11/pybind11.h>

namespace py = pybind11;

class Pet {
public:
    Pet(const std::string &name) : name_(name) {}

    const std::string &getName() const { return name_; }
    void setName(const std::string &name) { name_ = name; }

private:
    std::string name_;
};

PYBIND11_MODULE(example, m) {
    py::class_<Pet>(m, "Pet")
        .def(py::init<const std::string &>())
        .def_property("name", &Pet::getName, &Pet::setName);
}

解释:

  • .def_property("name", &Pet::getName, &Pet::setName): 将name_成员变量暴露为Python属性name&Pet::getName是getter函数,&Pet::setName是setter函数。

Python代码:

import example

pet = example.Pet("Muffin")
print(pet.name)  # 输出: Muffin
pet.name = "Rover"
print(pet.name)  # 输出: Rover

绑定静态方法 (Static Methods)

C++类中的静态方法也可以绑定到 Python:

#include <pybind11/pybind11.h>

namespace py = pybind11;

class MyClass {
public:
    static int myStaticMethod(int x) {
        return x * 2;
    }
};

PYBIND11_MODULE(example, m) {
    py::class_<MyClass>(m, "MyClass")
        .def(py::init<>()) // 必须要有构造函数,即使是默认构造
        .def_static("static_method", &MyClass::myStaticMethod);
}

Python代码:

import example

result = example.MyClass.static_method(5)
print(result)  # 输出:10

绑定重载函数 (Overloaded Functions)

C++支持函数重载,Pybind11也支持:

#include <pybind11/pybind11.h>

namespace py = pybind11;

int add(int i, int j) {
    return i + j;
}

double add(double i, double j) {
    return i + j;
}

PYBIND11_MODULE(example, m) {
    m.def("add", py::overload_cast<int, int>(&add), "Add two integers");
    m.def("add", py::overload_cast<double, double>(&add), "Add two doubles");
}

Python代码:

import example

print(example.add(1, 2))       # 输出: 3
print(example.add(1.5, 2.5))   # 输出: 4.0

绑定 STL 容器

Pybind11可以让你在Python中使用C++的STL容器,比如std::vectorstd::liststd::map等等。

C++代码 (example.cpp):

#include <pybind11/pybind11.h>
#include <pybind11/stl.h> // 包含这个头文件

#include <vector>

namespace py = pybind11;

std::vector<int> create_vector() {
    return {1, 2, 3, 4, 5};
}

PYBIND11_MODULE(example, m) {
    m.def("create_vector", &create_vector);
}

Python代码:

import example

my_vector = example.create_vector()
print(my_vector)  # 输出: [1, 2, 3, 4, 5]

处理异常

C++代码中可能会抛出异常,Pybind11可以自动将C++异常转换为Python异常。

C++代码 (example.cpp):

#include <pybind11/pybind11.h>

namespace py = pybind11;

int divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Division by zero!");
    }
    return a / b;
}

PYBIND11_MODULE(example, m) {
    m.def("divide", &divide);
}

Python代码:

import example

try:
    result = example.divide(10, 0)
except ZeroDivisionError as e:
    print(e)  # 输出: Division by zero!
except Exception as e:
    print(type(e).__name__, e) # 打印异常类型和信息

高级用法

  • NumPy支持: Pybind11可以与NumPy无缝集成,让你在C++中直接操作NumPy数组,这对于科学计算非常有用。
  • 自定义类型转换: 如果你需要处理一些特殊的类型,Pybind11允许你自定义类型转换规则。
  • 多线程: Pybind11支持多线程,你可以在C++中使用多线程来提高性能,然后将结果返回给Python。

总结

Pybind11是一个非常强大的工具,它可以让你轻松地将C++代码暴露给Python,从而兼顾性能和易用性。如果你需要用Python调用C++代码,Pybind11绝对是你的不二之选。

常见问题与解答

问题 解答
编译时出现找不到pybind11头文件错误 确保已经安装了pybind11,并且在编译命令中包含了pybind11的头文件路径。 使用python3 -m pybind11 --includes获取正确的头文件路径。
运行时出现ImportError 确保编译生成的.so (Linux/macOS) 或者 .pyd (Windows) 文件在Python的搜索路径中。 可以将文件放在与Python脚本相同的目录下,或者添加到PYTHONPATH环境变量中。
如何处理C++中的智能指针 Pybind11可以直接处理std::shared_ptrstd::unique_ptr。 你需要在绑定类的时候使用py::return_value_policy::reference 或者 py::return_value_policy::move来控制Python对象对C++对象的生命周期管理。 前者返回引用,后者转移所有权。
如何调试Pybind11代码 调试Pybind11代码比较麻烦,因为涉及到C++和Python之间的交互。 可以使用GDB (Linux/macOS) 或者 Visual Studio (Windows) 来调试C++代码。 在Python代码中可以使用pdb来调试Python代码。 建议使用日志来辅助调试,在C++代码中打印日志信息,然后在Python代码中查看日志。
如何处理复杂的C++模板类 Pybind11支持绑定C++模板类,但是需要显式地指定模板参数。 例如,如果要绑定std::vector<int>,你需要使用py::class_<std::vector<int>>(m, "IntVector")
如何处理C++的回调函数 Pybind11支持将Python函数作为回调函数传递给C++代码。 你需要使用std::function来封装Python函数,然后在C++代码中调用该函数。 注意,在C++中调用Python函数时,需要获取GIL (Global Interpreter Lock),以避免多线程问题。

希望这个讲座能帮助你入门Pybind11,让你的Python和C++“喜结良缘”! 祝各位编码愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注