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

好的,各位观众老爷们,今天咱们来聊聊一个能让你的 Python 技能瞬间“升华”的利器:pybind11! 啥?你还不知道 pybind11 是啥? 别急,听我慢慢道来。

啥是 pybind11?

简单来说,pybind11 就是一个 C++ 库,专门用来把你的 C++ 代码“翻译”成 Python 能看懂的“语言”。 想象一下,你辛辛苦苦用 C++ 写了一个高性能的算法库,但是你的 Python 朋友们想用怎么办?难道让他们也去学 C++ 吗?太残忍了! 这时候,pybind11 就闪亮登场了,它可以让你轻松地把 C++ 代码封装成 Python 模块,让 Python 程序员也能享受到 C++ 的速度和效率。

更通俗点说,pybind11 就是一个“翻译官”,它把 C++ 代码翻译成 Python 代码,让 Python 和 C++ 能够无缝衔接,愉快地玩耍。

为啥要用 pybind11?

你可能会问,Python 本身就很好用啊,为啥还要用 C++ 呢? 好问题! 答案很简单:速度!

Python 是一种解释型语言,执行速度相对较慢。 而 C++ 是一种编译型语言,执行速度非常快。 对于一些计算密集型的任务,比如图像处理、科学计算、机器学习等,用 C++ 来实现可以显著提高程序的运行效率。

但是,Python 的语法简单易学,生态系统非常丰富,有很多强大的库可以使用。 如果能把 C++ 的速度和 Python 的易用性结合起来,岂不是美滋滋? 这就是 pybind11 的价值所在。

总结一下,使用 pybind11 的好处有:

  • 提高程序运行速度: C++ 代码的执行速度比 Python 快得多。
  • 重用现有 C++ 代码: 可以把现有的 C++ 库封装成 Python 模块,避免重复造轮子。
  • 利用 C++ 的底层能力: 可以访问操作系统的底层 API,实现更强大的功能。
  • 混合编程的乐趣: 可以把 Python 和 C++ 的优点结合起来,创造出更优秀的程序。

pybind11 的特点

pybind11 相比于其他 C++ 到 Python 的绑定工具,例如 Boost.Python,有以下优点:

  • 轻量级: 只有一个头文件,易于集成到项目中。
  • 现代 C++: 充分利用了 C++11 及更高版本的特性,代码简洁易懂。
  • 易于使用: API 设计简洁明了,学习曲线平缓。
  • 高性能: 绑定代码的开销很小,不会影响程序的性能。
  • 类型安全: 提供了丰富的类型转换功能,保证了类型安全。

安装 pybind11

安装 pybind11 非常简单,只需要使用 pip 命令即可:

pip install pybind11

一个简单的例子

接下来,我们来看一个简单的例子,演示如何使用 pybind11 把一个 C++ 函数封装成 Python 模块。

首先,创建一个名为 example.cpp 的文件,包含以下代码:

#include <pybind11/pybind11.h>

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

namespace py = pybind11;

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

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

这段代码定义了一个名为 add 的 C++ 函数,它接受两个整数作为参数,并返回它们的和。 PYBIND11_MODULE 宏定义了一个 Python 模块,名为 example,并将 add 函数暴露给 Python。

接下来,我们需要创建一个 setup.py 文件,用于编译 C++ 代码并生成 Python 模块。 setup.py 文件的内容如下:

from setuptools import setup, Extension
from pybind11.setup_helpers import Pybind11Extension, build_ext

ext_modules = [
    Pybind11Extension("example", ["example.cpp"]),
]

setup(
    name="example",
    version="0.0.1",
    ext_modules=ext_modules,
    cmdclass={"build_ext": build_ext},
)

这个 setup.py 文件使用了 setuptoolspybind11.setup_helpers 模块来编译 C++ 代码。 Pybind11Extension 类用于定义一个 pybind11 扩展模块,第一个参数是模块的名称,第二个参数是 C++ 代码的文件列表。

最后,我们可以使用以下命令来编译 C++ 代码并生成 Python 模块:

python setup.py build_ext --inplace

这个命令会在当前目录下生成一个名为 example.so (Linux) 或 example.pyd (Windows) 的文件,这就是我们生成的 Python 模块。

现在,我们可以在 Python 中导入 example 模块,并调用 add 函数:

import example

result = example.add(1, 2)
print(result)  # 输出:3

恭喜你!你已经成功地把一个 C++ 函数封装成 Python 模块,并从 Python 中调用了它。

更高级的用法

上面的例子只是 pybind11 的一个简单入门。 pybind11 还提供了很多更高级的功能,例如:

  • 绑定类: 可以把 C++ 类封装成 Python 类,让 Python 程序员可以创建 C++ 类的对象,并调用它们的方法。
  • 处理 STL 容器: 可以把 C++ 的 STL 容器(例如 std::vectorstd::map)转换成 Python 的列表和字典。
  • 处理 NumPy 数组: 可以把 C++ 的数组和 NumPy 数组相互转换,方便进行科学计算。
  • 处理异常: 可以把 C++ 的异常转换成 Python 的异常,让 Python 程序员可以捕获 C++ 代码抛出的异常。
  • 使用智能指针: 可以使用 C++ 的智能指针(例如 std::shared_ptrstd::unique_ptr)来管理对象的生命周期,避免内存泄漏。

下面我们分别来看一些例子:

绑定类

#include <pybind11/pybind11.h>

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

namespace py = pybind11;

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() + "'>";
             }
        );
}

在这个例子中,我们定义了一个名为 Pet 的 C++ 类,并使用 py::class_ 模板类把它封装成 Python 类。 def 方法用于定义类的方法, py::init<> 用于定义类的构造函数。 __repr__ 是 Python 的特殊方法,用于定义对象的字符串表示。

在 Python 中,我们可以这样使用 Pet 类:

import example

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

处理 STL 容器

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <vector>

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

namespace py = pybind11;

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

在这个例子中,我们定义了一个名为 get_vector 的 C++ 函数,它返回一个包含整数的 std::vector。 为了让 pybind11 能够处理 STL 容器,我们需要包含 <pybind11/stl.h> 头文件。

在 Python 中,我们可以这样使用 get_vector 函数:

import example

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

处理 NumPy 数组

#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <iostream>

namespace py = pybind11;

py::array_t<double> add_arrays(py::array_t<double> a, py::array_t<double> b) {
    py::buffer_info a_buf = a.request();
    py::buffer_info b_buf = b.request();

    if (a_buf.ndim != 1 || b_buf.ndim != 1)
        throw std::runtime_error("Number of dimensions must be one");

    if (a_buf.size != b_buf.size)
        throw std::runtime_error("Input shapes must match");

    auto result = py::array_t<double>(a_buf.size);
    py::buffer_info result_buf = result.request();

    double *a_ptr = (double *) a_buf.ptr;
    double *b_ptr = (double *) b_buf.ptr;
    double *res_ptr = (double *) result_buf.ptr;

    for (size_t i = 0; i < a_buf.size; i++)
        res_ptr[i] = a_ptr[i] + b_ptr[i];

    return result;
}

PYBIND11_MODULE(example, m) {
    m.def("add_arrays", &add_arrays, "Add two NumPy arrays");
}

在这个例子中,我们定义了一个名为 add_arrays 的 C++ 函数,它接受两个 NumPy 数组作为参数,并返回它们的和。 为了让 pybind11 能够处理 NumPy 数组,我们需要包含 <pybind11/numpy.h> 头文件。 我们使用 py::array_t 模板类来表示 NumPy 数组,并使用 request() 方法来获取数组的缓冲区信息。

在 Python 中,我们可以这样使用 add_arrays 函数:

import example
import numpy as np

a = np.array([1.0, 2.0, 3.0])
b = np.array([4.0, 5.0, 6.0])
result = example.add_arrays(a, b)
print(result)  # 输出:[5. 7. 9.]

异常处理

#include <pybind11/pybind11.h>

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

namespace py = pybind11;

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

在这个例子中,我们定义了一个名为 divide 的 C++ 函数,它接受两个整数作为参数,并返回它们的商。 如果除数为 0,则抛出一个 std::runtime_error 异常。 pybind11 会自动把 C++ 异常转换成 Python 异常。

在 Python 中,我们可以这样使用 divide 函数:

import example

try:
    result = example.divide(10, 0)
except RuntimeError as e:
    print(e)  # 输出:Division by zero!
else:
    print(result)

使用智能指针

#include <pybind11/pybind11.h>
#include <memory>

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

namespace py = pybind11;

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

在这个例子中,我们使用 std::shared_ptr 来管理 Pet 对象的生命周期。 在 py::class_ 模板类中,我们将第二个模板参数设置为 std::shared_ptr<Pet>,表示 Python 代码持有的是 Pet 对象的共享指针。 这样,当 Python 代码不再使用 Pet 对象时,std::shared_ptr 会自动释放对象的内存,避免内存泄漏。

一些实用技巧

  • 使用 PYBIND11_MODULE 宏: 这是定义 Python 模块的关键。 确保模块名称与 setup.py 文件中的模块名称一致。
  • 添加文档字符串: 使用 m.doc() 为模块添加文档字符串,使用 m.def() 的第三个参数为函数添加文档字符串。 这样,Python 程序员可以使用 help() 函数来查看模块和函数的文档。
  • 处理 C++ 引用和指针: pybind11 提供了多种方法来处理 C++ 引用和指针。 默认情况下,pybind11 会把 C++ 引用和指针转换成 Python 对象。 如果需要返回 C++ 对象本身,可以使用 py::return_value_policy::referencepy::return_value_policy::copy
  • 使用 py::arg 可以使用 py::arg 为函数的参数指定名称和默认值。 这样,Python 程序员可以更方便地调用函数。
  • 使用 py::enum_ 可以把 C++ 的枚举类型封装成 Python 的枚举类型。

高级主题

  • 自定义类型转换: 如果 pybind11 提供的类型转换功能不能满足你的需求,你可以自定义类型转换函数。
  • 使用 CMake: 可以使用 CMake 来管理 pybind11 项目。
  • 与其他库集成: 可以把 pybind11 与其他 C++ 库集成,例如 Eigen、OpenCV 等。

总结

pybind11 是一个非常强大的 C++ 到 Python 的绑定工具。 它可以让你轻松地把 C++ 代码封装成 Python 模块,让 Python 程序员也能享受到 C++ 的速度和效率。 无论你是想提高程序的运行速度,还是想重用现有的 C++ 代码,pybind11 都是一个值得学习的工具。

希望今天的讲解对你有所帮助! 感谢大家的观看,下次再见!

发表回复

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