在mac上使用pybind11实现c++与python互通的demo
0. 问题描述
下面的python代码定义了一个类,类中有一个名为analyze_data的函数,其采用递归的方法解析json格式的data数据,由于处理速度达不到要求,我们希望用c++改写该类,并封装成可供python代码调用的库,以达到加速的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| data_demo = {"resbody": {"data": {"name": "John", "age": 30, "address": {"city": "New York", "zipcode": 10001}}}} class AnalyzeDataUtil: def __init__(self, datas): self.datas = datas def analyze_data(self, data, result="resbody", depth=0): if depth == 5: return if isinstance(data, dict): for k, v in data.items(): self.analyze_data(v, result + "$%s" % str(k), depth+1) if isinstance(data, (list, tuple)): for i in range(len(data)): self.analyze_data(data[i], result, depth+1) else: self.datas[result] = str(data)
|
问题分析:
根据问题描述,首先,我们要用c++对python定义的类进行改写,由于传入的变量data类型不固定,且涉及到类型判断,一种比较简单的方式是采用pybind11,它针对python的常见数据类型,都有对应的c++实现版本,代码改写工作大大减少。其次,类改写好之后,我们需要打通c++与python之间的隔阂,使用pybind11实现起来也非常方便,只需要在c++源码中添加少量代码即可。最后,我们还需要将c++源码编译成共享库,供下游代码调用。
1. 安装必要的工具和包
1 2
| brew install cmake conda install pybind11
|
注意:安装pybind11的时候要用conda来安装,用pip方式安装,编译的时候会报路径错误。
2. 创建c++源文件
下面的代码实现了AnalyzeDataUtil类的c++版本,第54行到第60行的作用是打通c++与python之间的隔阂。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| #include <pybind11/pybind11.h> #include <pybind11/stl.h> namespace py = pybind11; struct AnalyzeDataUtil { public: AnalyzeDataUtil(py::dict datas) : datas(datas) {} void analyze_data(const py::object &data, std::string result = "", int depth = 0) { if (depth == 5) return; if (py::isinstance<py::dict>(data)) { py::dict dict_data = py::cast<py::dict>(data); for (const auto &item : dict_data) { auto key = py::reinterpret_borrow<py::object>(item.first); auto value = py::reinterpret_borrow<py::object>(item.second); analyze_data(value, result + "$" + py::str(key).cast<std::string>(), depth + 1); } } if (py::isinstance<py::list>(data) || py::isinstance<py::tuple>(data)) { py::list list_data = py::cast<py::list>(data); for (const auto &item : list_data) { analyze_data(py::cast<py::object>(item), result, depth + 1); } } else { datas[py::str(result)] = py::str(data).cast<std::string>(); } } py::dict get_datas() { return datas; } private: py::dict datas; }; PYBIND11_MODULE(analyze_data_util, m) { py::class_<AnalyzeDataUtil>(m, "AnalyzeDataUtil") .def(py::init<py::dict>()) .def("analyze_data", &AnalyzeDataUtil::analyze_data) .def("get_datas", &AnalyzeDataUtil::get_datas); }
|
4. 创建CMakeLists.txt文件编译C++模块
创建文件analyze_data_util/CMakeLists.txt, 然后将下面的脚本拷贝到文件中。
1 2 3 4 5 6 7 8 9
| cmake_minimum_required(VERSION 3.12)
project(analyze_data_util LANGUAGES CXX) set(CMAKE_CXX_STANDARD 14) find_package(pybind11 REQUIRED) pybind11_add_module(analyze_data_util analyze_data_util.cpp)
|
5. 编译C++模块
1 2 3 4
| mkdir build cd build cmake .. cmake --build .
|
将工作目录切换到analyze_data_util中,分别执行上面的代码。将会创建共享库文件,在Mac或Linux中名为:analyze_data_util.so,在Windows中为analyze_data_util.pyd
6. 构造接口
因为在c++中不允许直接访问私有成员,因此我们实现了get_datas()方法来间接访问成员,但是这不符合python的使用习惯,因此我们构造一个同名的AnalyzeDataUtil类将这一细节隐藏起来。也可以省去该步奏,但必须通过get_datas()函数访问解析后的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import analyze_data_util class AnalyzeDataUtil: def __init__(self, datas): self.analyzer = analyze_data_util.AnalyzeDataUtil(datas) self.datas = datas def analyze_data(self, data, result="resbody", depth=0): self.analyzer.analyze_data(data, result, depth) self.datas = self.analyzer.get_datas()
|
7. 在python中使用C++模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| from analyze_data_tool import AnalyzeDataUtil import json import time import pprint print('示例用法,c++版本:') json_data = '{"resbody": {"data": {"name": "John", "age": 30, "address": {"city": "New York", "zipcode": 10001}}}}' data_dict = json.loads(json_data) time1 = time.time() analyzer2 = AnalyzeDataUtil({}) analyzer2.analyze_data(data_dict, "resbody", 0) time2 = time.time() all_keys1 = analyzer2.datas.keys() keys_list1 = ' '.join(list(all_keys1)) result = time2 - time1 formatted_result = f"{result:.10f}" pp.pprint(all_keys1) print(formatted_result)
|
创建以上代码,然后将生成的.so or .pyd文件拷贝到demo/目录下,运行python test.py。
8. 后记
虽然上面的解决方案实现了c++与python的互通,但我在对单个文件进行测试比较的时候,得到如下结果:

从结果中我们发现,对于相同的json文件,c++版本与python版本处理结果是一致的,但对于速度的提升并不理想,对于单个文件的处理c++的版本甚至比python的低,可能是因为python调用c++包的时候占用了更多的时间,因此,处理速度的提升体现不出来。针对这个问题,我们至少可以从以下两个方面进行考虑:首先,从算法层面上来看,我们可以将递归改为动态规划,或者对递归过程进行剪枝处理,因为递归虽然好用,但是效率往往不高,它重复的过程实比较多。其次,我们还可以尝试使用一些能够快速解析json格式的优秀库(比如RapidJSON),这或许会比使用我们自己写的粗糙代码效率来的更快。
最后,虽然上面讲解中涉及到的每个脚本,我都给出了它所在的文件名和位置,但是并不直观,为了方便大家更直观的理解(我跑别人博客代码的时候,经常因为位置放错,浪费时间去调试),我把工程目录粘贴出来:

以上就是使用pybind11实现c++与python互通的demo啦,希望能够帮到你,想要了解更多,可以去看下面的官方文档喔。