茄子的个人空间

在mac上使用pybind11实现c++与python互通的demo

字数统计: 1.8k阅读时长: 8 min
2023/07/23 2

在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
 # 待解析的数据deom
 data_demo = {"resbody": {"data": {"name": "John", "age": 30, "address": {"city": "New York", "zipcode": 10001}}}}
 
 # 需要转换为c++代码的类
 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
 //path: analyze_data_util/analyze_data_util.cpp
 #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
 # path: demo/analyze_data_tool.py
 import analyze_data_util
 
 # Wrapper for AnalyzeDataUtil class
 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
 # path: demo/test.py
 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的互通,但我在对单个文件进行测试比较的时候,得到如下结果:

image-20230723015325241

从结果中我们发现,对于相同的json文件,c++版本与python版本处理结果是一致的,但对于速度的提升并不理想,对于单个文件的处理c++的版本甚至比python的低,可能是因为python调用c++包的时候占用了更多的时间,因此,处理速度的提升体现不出来。针对这个问题,我们至少可以从以下两个方面进行考虑:首先,从算法层面上来看,我们可以将递归改为动态规划,或者对递归过程进行剪枝处理,因为递归虽然好用,但是效率往往不高,它重复的过程实比较多。其次,我们还可以尝试使用一些能够快速解析json格式的优秀库(比如RapidJSON),这或许会比使用我们自己写的粗糙代码效率来的更快。

最后,虽然上面讲解中涉及到的每个脚本,我都给出了它所在的文件名和位置,但是并不直观,为了方便大家更直观的理解(我跑别人博客代码的时候,经常因为位置放错,浪费时间去调试),我把工程目录粘贴出来:

image-20230723022223900

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

CATALOG
  1. 1. 在mac上使用pybind11实现c++与python互通的demo