C++/Python混合编程

以 C++ 为底层基础,Python 作为上层建筑,共同搭建起高性能、易维护、可扩展的混合系统。

Python 本身就有 C 接口,可以用 C 语言编写扩展模块,把一些低效耗时的功能改用 C 实现,有的时候,会把整体性能提升几倍甚至几十倍。

使用 C++ 来开发 Python 扩展。认为其中最好的一个就是pybind11

pybind11 借鉴了“前辈”Boost.Python,能够在 C++ 和 Python 之间自由转换,任意翻译两者的语言要素,比如把 C++ 的 vector 转换为 Python 的列表,把 Python 的元组转换为 C++ 的 tuple,既可以在 C++ 里调用 Python 脚本,也可以在 Python 里调用 C++ 的函数、类。

pybind11 名字里的“11”表示它完全基于现代 C++ 开发(C++11 以上),所以没有兼容旧系统的负担。它使用了大量的现代 C++ 特性,不仅代码干净整齐,运行效率也更高。

接下来看下,如何用Pybind11,让C++辅助Python,提升Python的性能



pybind11是一个纯头文件的库,但因为必须结合Python,所以首先要有Python的开发库,然后在用pip工具安装。

pybind11 支持 Python2.7、Python3 和 PyPy,这里用的是 Python3:

apt-get install python3-dev
apt-get install python3-pip
pip3 install pybind11

pybind11 充分利用了 C++ 预处理和模板元编程,把原本无聊重复的代码都隐藏了起来,展现了“神奇的魔法”——只需要短短几行代码,就可以实现一个 Python 扩展模块。具体怎么实现呢?

只要用一个宏“PYBIND11_MODULE”,再给它两个参数,Python 模块名和 C++ 实例对象名,就可以了。

#include <pybind11/pybind11.h>        // pybind11的头文件

PYBIND11_MODULE(pydemo, m)            // 定义Python模块pydemo
{
  m.doc() = "pybind11 demo doc";      // 模块的说明文档
}                                     // Python模块定义结束

代码里的 pydemo 就是 Python 里的模块名,之后在 Python 脚本里必须用这个名字才能 import。

第二个参数“m”其实是 pybind11::module 的一个实例对象,封装了所有的操作,比如这里的 doc() 就是模块的说明文档。它只是个普通的变量,起什么名字都可以,但为了写起来方便,一般都用“m”。

假设这个 C++ 源文件名是“pybind.cpp”,现在你就可以用 g++ 把它编译成在 Python 里调用的模块了,不过编译命令比较复杂:

g++ pybind.cpp               \                  #编译的源文件
   -std=c++11 -shared -fPIC   \                 #编译成动态库
  `python3 -m pybind11 --includes` \            #获得包含路径
  -o pydemo`python3-config --extension-suffix`  #生成的动态库名字

第一行是指定编译的源文件,第二行是指定编译成动态库,
第三行调用了 Python,获得 pybind11 所在的包含路径,让 g++ 能够找得到头文件。第四行最关键,是生成的动态库名字,前面必须是源码里的模块名,而后面那部分则是 Python 要求的后缀名,否则 Python 运行时会找不到模块。

编译完后会生成一个大概这样的文件:pydemo.cpython-35m-x86_64-linux-gnu.so,现在就可以在 Python 里验证了,使用 import 导入,然后用 help 就能查看模块说明:

$ python3
>>> import pydemo
>>> help(pydemo)

刚才的代码非常简单,只是个空模块,里面什么都没有,现在来看看怎么把 C++ 的函数导入 Python

需要用的是 def() 函数,传递一个 Python 函数名和 C++ 的函数、函数对象或者是 lambda 表达式,形式上和 Python 的函数也差不多

namespace py = pybind11;               // 名字空间别名,简化代码

PYBIND11_MODULE(pydemo, m)             // 定义Python模块pydemo
{
  m.def("info",                        // 定义Python函数
    []()                              // 定义一个lambda表达式
    {
      py::print("c++ version =", __cplusplus);  // pybind11自己的打印函数
      py::print("gcc version =", __VERSION__);
      py::print("libstdc++   =", __GLIBCXX__);
    }
  );  

  m.def("add",                       // 定义Python函数
    [](int a, int b)                 // 有参数的lambda表达式
    {
      return a + b;
    }
  );  
}                                     // Python模块定义结束

在 Python 里可以验证效果:

import pydemo            # 导入pybind11模块
pydemo.info()            # 调用C++写的函数
x = pydemo.add(1,2)      # 调用C++写的函数

pybind11 也支持函数的参数、返回值使用标准容器,会自动转换成 Python 里的 list、dict,不过你需要额外再包含一个“stl.h”的头文件。

下面的示例代码演示了 C++ 的 string、tuple 和 vector 是如何用于 Python 的:

#include <pybind11/stl.h>              // 转换标准容器必须的头文件

PYBIND11_MODULE(pydemo, m)             // 定义Python模块pydemo
{
  m.def("use_str",                     // 定义Python函数
    [](const string& str)              // 入参是string
    {
        py::print(str);
        return str + "!!";            // 返回string
    }
  );  

  m.def("use_tuple",                  // 定义Python函数
    [](tuple<int, int, string> x)     // 入参是tuple
    {
        get<0>(x)++;
        get<1>(x)++;
        get<2>(x)+= "??";
        return x;                     // 返回元组
    }
  );  

  m.def("use_list",                   // 定义Python函数
    [](const vector<int>& v)          // 入参是vector
    {
        auto vv = v;
        py::print("input :", vv);
        vv.push_back(100);
        return vv;                    // 返回列表       
    }
  );
}

因为都是面向对象的编程语言,C++ 里的类也能够等价地转换到 Python 里面调用,这要用到一个特别的模板类 class_,注意,它有意模仿了关键字 class,后面多了一个下划线。

拿一个简单的 Point 类来举个例子:

class Point final
{
public:
    Point() = default;
    Point(int a);
public:
    int get() const;
    void set(int a);
};

使用 pybind11,你需要在模板参数里写上这个类名,然后在构造函数里指定它在 Python 里的名字。

导出成员函数还是调用函数 def(),但它会返回对象自身的引用,所以就可以连续调用,在一句话里导出所有接口:

py::class_<Point>(m, "Point")           // 定义Python类
    .def(py::init())                    // 导出构造函数
    .def(py::init<int>())               // 导出构造函数
    .def("get", &Point::get)            // 导出成员函数
    .def("set", &Point::set)            // 导出成员函数
    ;

对于一般的成员函数来说,定义的方式和普通函数一样,只是你必须加上取地址操作符“&”,把它写成函数指针的形式。而构造函数则比较特殊,必须调用 init() 函数来表示,如果有参数,还需要在 init() 函数的模板参数列表里写清楚

小试牛刀:可参考官方文档,学习其他具体方法。

参考:
https://time.geekbang.org/column/article/245905

posted @ 2022-10-05 20:26  牛犁heart  阅读(1530)  评论(0编辑  收藏  举报