C++和Python混合编程之Pybind11的简单使用

avatar
作者
筋斗云
阅读量:0

C++和Python混合编程之Pybind11的简单使用

一、简介

Pybind11是C++/Python混合编程的利器之一,是一个轻量级的只包含头文件的库,用于 Python 和 C++ 之间接口转换,可以为现有的 C++ 代码创建 Python 接口绑定。Pybind11 名字里的“11”表示它完全基于现代 C++ 开发(C++11 以上),所以没有兼容旧系统的负担。它使用了大量的现代 C++ 特性,不仅代码干净整齐,运行效率也更高。


二、平台环境

1、系统:Windows10
2、Python虚拟环境工具:Anaconda3
3、C++ IDE:Visual Studio 2022
4、Python版本:3.7.16


三、C++/Python相互调用的方法

简单介绍如何实现两种语言之间相互调用

1、Python调用C++代码: 通过调用动态库的方式完成,将C++代码编译生成动态库文件(Win下为.DLL),Python调用的话需要将库后缀改为(.pyd),然后将动态库拷贝到Python文件主目录,代码内导入库模块即可;

2、C++代码调用Python: 主要通过调用Python代码解释器来实现。


四、代码实践

用代码实例简单展现Pybind11的功能

1、基础环境搭建

1.1、安装Pybind11库

有多种安装方式,这里通过pip命令来安装,如果使用了虚拟环境,安装前记得激活相应的虚拟环境:

安装命令如下:

(py37) C:\Users\xxx> pip install pybind11 

1.2、Visual Studio项目属性配置:

具体路径根据自己项目实际情况而定
1). 通用编译属性设置:

  • 属性–>常规–>常规属性–>配置类型:动态库(.dll)
  • 属性–>高级–>高级属性–>目标文件扩展名:.pyd

2). C/C++附加包含目录include:

  • 属性–>C/C++ -->常规–>附加包含目录:
  • D:\Anaconda3\envs\py37\include
  • D:\Anaconda3\envs\py37\Lib\site-packages\pybind11\include

3). 链接器附加库目录和库文件:

  • 属性–>链接器–>常规–>附加库目录:D:\Anaconda3\envs\py37\libs
  • 属性–>链接器–>输入–>附加依赖项:python3.lib,python37.lib

具体操作如下:
配置类型
目标文件扩展名
附加包含目录

附加库目录
附加依赖项


1.3、系统环境变量设置

1)、因为在C++调用Python代码过程中遇到错误,经过查资料找到了解决办法(stackoverflow讨论地址),以下环境具体路径根据自己项目实际情况而定。
2)、要在C++中调用Python解释器(py::scoped_interpreter guard{};),需要添加两个系统环境变量,以便Pybind11能够找到解释器位置:

  • PYTHONHOME:D:\Anaconda3\envs\py37
  • PYTHONPATH:D:\Anaconda3\envs\py37\Lib;D:\Anaconda3\envs\py37\Lib\site-packages;D:\Anaconda3\envs\py37\DLLs

3)、如果不设置这两个环境变量会出现以下错误:

  • Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
    Python runtime state: core initialized
    ModuleNotFoundError: No module named ‘encodings’

4)、设置完后重启电脑生效

错位描述
从打印的错误可以看出[PYTHONHOME,PYTHONPATH]两个系统环境变量未设置。

设置系统变量:
PYTHONHOME
PYTHONPATH
注意: 增加这两个环境变量后可能导致Anaconda3虚拟环境命令找不到,进而无法激活虚拟环境,如果出现则删除这两个环境变量即可(暂时没找到好的解决办法),删除后重启电脑。


2、Python使用C++代码动态库

演示两个流程:

  • C++编译动态库;
  • Python代码中调用动态库。
2.1、C++编译动态库

演示C++编译动态库以供Python调用

代码示例:

#include <iostream> #include <string> #include <tuple> #include <vector> #include <map> #include <pybind11/pybind11.h> #include <pybind11/stl.h>  // 转换标准容器必须的头文件  namespace py = pybind11;  // 名字空间别名,简化代码  class Point final { private: 	int x = 0; public: 	Point() = default; 	~Point() = default; 	Point(int a) : x(a) {} public: 	int get() const 	{ 		return x; 	}  	void set(int a) 	{ 		x = a; 	} };  // 用lambda表达式来测试 PYBIND11_MODULE(pydemo, m)  // 定义Python模块pydemo { 	m.doc() = "pybind11 demo doc"; 	 	m.def("info", 		[]() 		{ 			py::print("c++ version: ", __cplusplus); 		} 	);  	m.def("add", 		[](int a, int b) 		{ 			return a + b; 		} 	);  	m.def("use_str", 		[](const std::string& str)  // 定义python函数,入参是string 		{ 			py::print(str); 			return str + "!!";  // 返回string 		} 	);  	m.def("use_tuple", 		[](std::tuple<int, int, std::string> x)  // 定义python函数,入参是tuple 		{ 			std::get<0>(x)++; 			std::get<1>(x)++; 			std::get<2>(x) += "??"; 			return x; 		} 	);  	m.def("use_list", 		[](std::vector<int>& v)  // 定义python函数,入参是vector 		{ 			auto vv = v; 			py::print("input :", vv); 			vv.push_back(100); 			vv.push_back(200); 			return vv; 		} 	);  	m.def("use_map", 		[](std::map<std::string, std::string>& m)  // 定义python函数,入参是map 		{ 			auto mm = m; 			py::print("input : ", mm); 			mm["name"] = "LiMing"; 			mm["gender"] = "male"; 			return mm; 		} 	);  	// C++ 里的类也能够等价地转换到 Python 里面调用,这要用到一个特别的模板类 class_ 	py::class_<Point>(m, "Point")  // 定义Python类 		.def(py::init())           // 导出构造函数 		.def(py::init<int>())      // 导出构造函数 		.def("get", &Point::get)   // 导出成员函数 		.def("set", &Point::set)   // 导出成员函数 		; }  #if 0  // 用普通函数来测试 void info() { 	std::cout << "c++ version: " << __cplusplus << std::endl; }  int add(int a, int b) { 	return a + b; }  PYBIND11_MODULE(pydemo, m) { 	m.doc() = "pybind11 demodoc";  	m.def("info", &info, "cpp info"); 	m.def("add", &add, "add func"); }  #endif  #if 0 int main() { 	return 0; } #endif 

编译输出结果如下:

生成开始于 13:12... 1>------ 已启动生成: 项目: PythonAndCPP, 配置: Release x64 ------ 1>main.cpp 1>  正在创建库 F:\code\CPPdemo\PythonAndCPP\x64\Release\PythonAndCPP.lib 和对象 F:\code\CPPdemo\PythonAndCPP\x64\Release\PythonAndCPP.exp 1>正在生成代码 1>441 of 3091 functions (14.3%) were compiled, the rest were copied from previous compilation. 1>  186 functions were new in current compilation 1>  391 functions had inline decision re-evaluated but remain unchanged 1>已完成代码的生成 1>PythonAndCPP.vcxproj -> F:\code\CPPdemo\PythonAndCPP\x64\Release\PythonAndCPP.pyd ========== 生成: 1 成功,0 失败,0 最新,0 已跳过 ========== ========== 生成 于 13:12 完成,耗时 03.362 秒 ========== 

结果


2.2、在Python中调用动态库

首先需要将动态库拷贝到Python项目主目录下,然后才能在python代码中导入模块使用

首先需要将动态库拷贝到Python项目主目录下,如下图所示:
python项目
代码示例:

import pydemo  # 导入模块,与C++代码中定义的模块名一致  def test():     pydemo.info() 	 	// 调用add函数     print("Test add func: ")     print(pydemo.add(1,2)) 	 	// 调用Point类极其成员函数     print("Test Point class: ")     p = pydemo.Point(10)     print(p.get())     p.set(88)     print(p.get()) 	 	// 字符串转换测试:std::string->str     print("Test str: ")     print(pydemo.use_str("hello")) 	 	// 元组转换测试:std::tuple->tuple     print("Test tuple: ")     t = (11,22,"No")     print(pydemo.use_tuple(t)) 	 	// 列表转换测试:std::vector->list     print("Test list: ")     l = []     print(pydemo.use_list(l)) 	 	// 键值对转换测试:std::map->map     print("Test map: ")     m = {}     print(pydemo.use_map(m))       def main():     test()   if __name__ =="__main__":     main()      

运行结果如下:

(py37) F:\code\pydemo\Test>python main.py c++ version:  199711 Test add func:  3 Test Point class: 10 88 Test str: hello hello!! Test tuple: (12, 23, 'No??') Test list: input : [] [100, 200] Test map: input :  {} {'gender': 'male', 'name': 'LiMing'} 

3、C++调用Python解释器

用两种方式演示如何在C++代码中调用Python代码解释器:

  • 直接运行python代码
  • 导入python外部模块执行代码,更灵活

注意细节:

  • 需要将python代码文件拷贝至C++项目主目录下;
  • 将所用python版本的python3.dll,python37.dll两个动态库拷贝至C++可执行文件(.exe)所在目录,否则可能无法运行或运行出错。
3.1 拷贝代码运行所需文件

1)、拷贝python代码文件
拷贝python文件
2)、拷贝DLL文件
拷贝动态库


3.2、代码演示

代码示例:
python代码文件:pydemo.py
python代码:

import os from typing import List, AnyStr    def get_files(path:str) -> List[AnyStr]:     """     遍历目录下所有文件并返回结果     :param path: 目录     :return: 返回文件列表     """     if not os.path.exists(path):         return []          # 递归遍历文件夹下的所有文件     files = []     for (dirpath, dirnames, filenames) in os.walk(path):         files += filenames     return files    if __name__ == "__main__":     pass 

CPP代码:

#include <iostream> #include <string> #include <vector> #include <pybind11/pybind11.h> #include <pybind11/stl.h> #include <pybind11/embed.h>  // 要用解释器需要包含此头文件  namespace py = pybind11;  // 名字空间别名,简化代码  #if 1  void test_pybind11() { 	py::scoped_interpreter guard{};  // 初始化Python解释器  	// 1、直接运行python代码 	std::cout << "1、测试直接运行python代码:" << std::endl; 	try 	{ 		// 使用原始字符串R"()" 		py::exec(R"( def pow(a,n): 	return a**n)");  		auto func = py::module::import("__main__").attr("pow"); 		auto res = func(2, 3).cast<int>(); 		std::cout << "pow(2,3)函数输出结果如下:" << std::endl; 		std::cout << res << std::endl; 	} 	catch (const std::exception& e) 	{ 		std::cout << e.what() << std::endl; 	}  	std::cout << std::endl; 	 	// 2、导入python模块执行代码(pydemo.py),此种方法更灵活 	std::cout << "2、测试导入python模块执行外部代码:" << std::endl; 	try 	{ 		auto module = py::module::import("pydemo");  // 导入python外部模块pydemo,python中一个.py文件就是一个模块 		auto res = module.attr("get_files")("C:\\Users\\xxx\\Pictures\\wallpaper"); 		std::cout << "遍历文件如下: " << std::endl; 		for (const auto& val : res) 		{ 			std::cout << val << std::endl; 		} 		std::cout << std::endl; 	} 	catch (const std::exception& e) 	{ 		std::cout << e.what() << std::endl; 	} }  #endif  #if 1 int main() { 	test_pybind11(); 	return 0; } #endif 

运行结果如下:

1、测试直接运行python代码: pow(2,3)函数输出结果如下: 8  2、测试导入python模块执行外部代码: 遍历文件如下: img_1.png img_10.jpg img_11.jpg img_12.jpg img_13.png img_14.jpg  F:\code\CPPdemo\PythonAndCPP\x64\Release\PythonAndCPP.pyd (进程 17896)已退出,代码为 0。 按任意键关闭此窗口. . . 

五、结语

本文简单阐述了用Pybind11实现C++/Python混合编程的流程和注意事项,仅使用了Pybind11的一些简单功能,Pybind11功能很强大,需要其它更多功能请自行查询Pybind11官方文档

参考文章】:

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!