前言
近期的项目有从C++调用python脚本的需求,且Python脚本为引用了很多第三方包的复杂脚本,在网上查了很多资料都不全,很少涉及复杂脚本的引用(很多都是简单无第三方包的脚本),经过努力现已解决,故将解决思路写于此处
这一篇文章适合从零入手,如果需要用于项目实践,可以参考我在项目实践中整合的框架,详情可看:C++调用python优雅实践https://blog.youkuaiyun.com/Anzerwhite/article/details/147448515
环境准备
- Microsoft Visual Studio (笔者使用版本为2022 社区版)
- 确保机器安装了python解释器(笔者使用的为conda中的python3.10)
- 确保要调用的python脚本的依赖包都已导入(即都pip成功了)
- 运行时记得选择release模式,而不是debug!!
在VS中配置依赖库
创建新项目
在visual studio中创建一个新项目,如控制台应用程序(不建议选择空项目,可能会在配置依赖库时缺少选项)
配置链接目录
右键点击项目,选择属性,开始配置链接目录
设置附加包含目录
在“C/C++”选项卡下,找到“常规”部分,设置“附加包含目录”,将python解释器的包含目录(也就是python安装路径下的include文件夹)填入
注:笔者的python.exe路径为C:\Users\13680\.conda\envs\testForCpp2Py\python.exe,可作为相对路径的参考
找到待调用python脚本的环境下的python解释器的路径(一定要是相同python解释器版本),python.exe同级目录下的include文件夹就是我们的目标路径,将该路径复制后添加到“附加包含目录”这里(笔者此处路径为“C:\Users\13680\.conda\envs\testForCpp2Py\include”)
设置附加库目录
在“链接器”选项卡下,找到“常规”部分,设置“附加库目录”,将python解释器的库目录(也就是python安装路径下的libs文件夹)填入
将该libs文件夹的路径填入(笔者此处路径为“C:\Users\13680\.conda\envs\testForCpp2Py\libs”)
设置附加依赖项
在“链接器”选项卡下,找到“输入”部分,设置“附加依赖项”,将python解释器的对应的lib文件(也就是python安装路径下的libs文件夹中的python310.lib文件【根据python版本不同名称也不同,大致格式为pythonXXX.lib】)填入
python脚本准备
编写python脚本
这里笔者简单编写一个python脚本,其中导入了第三方包numpy
import numpy as np
print('enter python success') # 成功找到该脚本
def test_cpp2py(data):
"""
一个含参函数,用于测试cpp文件调用含第三方包的python脚本
:param data: 一个测试列表
:return:
"""
data_after_np = np.array(data)
print('successful, and data is: ', data_after_np) # 输出
return [1, 2, 3, 4] # 这里笔者偷懒就直接传一个固定列表返回给调用方了
将python脚本打包为.pyd文件
先创建一个setup.py文件,里面的内容为
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize("testForCpp2Py.py") # 这里改为你要打包成.pyd文件的py文件路径
)
然后在终端(控制台)执行以下命令:
python setup.py build_ext --inplace
执行后会在当前目录下生成几个文件,其中testForCpp2Py.cp310-win_amd64.pyd文件是我们的目标文件
至此我们完成了python端的准备,接下来就是编写Cpp接口文件了
编写C++接口
python解释器初始化
首先我们要在C++端初始化一个python解释器
Py_Initialize(); // 初始化 Python 解释器
导入.pyd文件和第三方包目录
然后,在这个步骤中最关键的就是将.pyd文件和第三方包导入,实现原理类似于设置环境变量(不懂也没关系,直接改好路径抄就行)
// 添加 .pyd 文件所在目录到 Python 路径
PyObject* sysPath = PySys_GetObject("path");
// 下面是为导入.pyd文件做准备
PyObject* pPath = PyUnicode_FromString("C:/Users/13680/PycharmProjects/srpTest"); // 这里改成.pyd文件所在目录的路径,原理类似于环境变量
// 下面是导入python脚本引入的第三方包
PyObject* pPath_packages = PyUnicode_FromString("C:/Users/13680/.conda/envs/srpTestEnd/Lib/site-packages"); // 将路径改为你调用的python脚本的解释器的 Lib/site-packages 目录
// 将上述路径加入python系统路径
PyList_Append(sysPath, pPath);
Py_DECREF(pPath);
PyList_Append(sysPath, pPath_packages);
Py_DECREF(pPath_packages);
注:这里的第三方包路径为python解释器的Lib/site-packages文件夹
调用.pyd文件
接下来就是正式导入.pyd文件并调用
注:Py_DECREF()函数用于指针释放,不影响业务
// 在这里导入你的 .pyd 文件,注意一定不要加后缀,比如testForCpp2Py.cp310-win_amd64.pyd就写testForCpp2Py就行
PyObject* pModule = PyImport_ImportModule("testForCpp2Py");
// 下面实现对.pyd文件中的函数的操作(前提是前面的操作没问题,pModule不会是空指针
if (pModule != nullptr) {
PyObject* pFunc = PyObject_GetAttrString(pModule, "test_cpp2py"); // 获取函数,这里填写要调用的函数名,笔者这里是test_cpp2py
if (pFunc && PyCallable_Check(pFunc)) { // 判断该函数是否存在且可调用
// 创建参数,由于c++和python的类型不对应,所以要转化
// 这里根据传入的vector类型的data来创建一个对应列表list
PyObject* pData = PyList_New(data.size());
for (int i = 0; i < data.size(); ++i) {
PyList_SetItem(pData, i, PyLong_FromLong(data[i])); // 这里要对data[i]转化为python类型
}
// 构建一个参数列表,第一个参数为参数列表的参数数量,这里为1
PyObject* pArgs = PyTuple_Pack(1, pData);
// 调用该函数
PyObject* pValue = PyObject_CallObject(pFunc, pArgs); // 调用函数
Py_DECREF(pArgs);
// 处理返回值
if (pValue != nullptr) {
// 声明一个结果的数组
vector<long> result{};
// 从pValue中取值
for (Py_ssize_t i = 0; i < PyList_Size(pValue); ++i) {
result.push_back(PyLong_AsLong(PyList_GetItem(pValue, i)));
}
Py_DECREF(pValue);
}
Py_DECREF(pFunc);
}
Py_DECREF(pModule);
}
else {
// 证明有问题,pModule为空指针了
PyErr_Print(); // 打印错误信息
}
关闭python解释器
用完后关闭python解释器即可
Py_Finalize(); // 关闭 Python 解释器
完整代码(C++)
#include <iostream>
#include <Python.h>
#include <vector>
using namespace std;
vector<int> test_cpp2py(vector<int> data) {
Py_Initialize(); // 初始化 Python 解释器
// 添加 .pyd 文件所在目录到 Python 路径
PyObject* sysPath = PySys_GetObject("path");
// 下面是为导入.pyd文件做准备
PyObject* pPath = PyUnicode_FromString("C:/Users/13680/PycharmProjects/testForCpp2Py"); // 这里改成.pyd文件所在目录的路径,原理类似于环境变量
// 下面是导入python脚本引入的第三方包
PyObject* pPath_packages = PyUnicode_FromString("C:/Users/13680/.conda/envs/testForCpp2Py/Lib/site-packages"); // 将路径改为你调用的python脚本的解释器的 Lib/site-packages 目录
// 将上述路径加入python系统路径
PyList_Append(sysPath, pPath);
Py_DECREF(pPath);
PyList_Append(sysPath, pPath_packages);
Py_DECREF(pPath_packages);
// 在这里导入你的 .pyd 文件,注意一定不要加后缀,比如testForCpp2Py.cp310-win_amd64.pyd就写testForCpp2Py就行
PyObject* pModule = PyImport_ImportModule("testForCpp2Py");
// 声明一个用于接收结果的数组
vector<int> result{};
// 下面实现对.pyd文件中的函数的操作(前提是前面的操作没问题,pModule不会是空指针
if (pModule != nullptr) {
PyObject* pFunc = PyObject_GetAttrString(pModule, "test_cpp2py"); // 获取函数,这里填写要调用的函数名,笔者这里是test_cpp2py
if (pFunc && PyCallable_Check(pFunc)) { // 判断该函数是否存在且可调用
// 创建参数,由于c++和python的类型不对应,所以要转化
// 这里根据传入的vector类型的data来创建一个对应列表list
PyObject* pData = PyList_New(data.size());
for (int i = 0; i < data.size(); ++i) {
PyList_SetItem(pData, i, PyLong_FromLong(data[i])); // 这里要对data[i]转化为python类型
}
// 构建一个参数列表,第一个参数为参数列表的参数数量,这里为1
PyObject* pArgs = PyTuple_Pack(1, pData);
// 调用该函数
PyObject* pValue = PyObject_CallObject(pFunc, pArgs); // 调用函数
Py_DECREF(pArgs);
// 处理返回值
if (pValue != nullptr) {
;
// 从pValue中取值
for (Py_ssize_t i = 0; i < PyList_Size(pValue); ++i) {
result.push_back(PyLong_AsLong(PyList_GetItem(pValue, i)));
}
Py_DECREF(pValue);
}
Py_DECREF(pFunc);
}
Py_DECREF(pModule);
}
else {
// 证明有问题,pModule为空指针了
PyErr_Print(); // 打印错误信息
}
Py_Finalize(); // 关闭 Python 解释器
return result; // 返回结果
}
int main()
{
vector<int> result = test_cpp2py({ 100, 200, 300, 400 });
cout << "\n上面为python脚本内的输出,下面为C++端的输出:\n";
cout << "\n返回值为:";
for (int item : result) {
cout << item << ' ';
}
cout << endl;
}
调用结果
可能存在的问题
无法重复调用函数问题
可能有些朋友会遇到连续调用两次 涉及C++调用Python 的函数,但只成功调用一次的情况,像下面这样:
int main()
{
vector<int> result = test_cpp2py({ 100, 200, 300, 400 });
vector<int> result_1 = test_cpp2py({ 100, 200, 300, 400 });
cout << "\n上面为python脚本内的输出,下面为C++端的输出:\n";
cout << "\n返回值为:";
for (int item : result) {
cout << item << ' ';
}
cout << endl;
for (int item : result_1) {
cout << item << ' ';
}
cout << endl;
}
这种情况可能是由于解释器的初始化和关闭的过程赶不上函数执行的过程导致的,这个时候可以尝试把解释器初始化和关闭的过程放到函数外面(记得把原来函数里的初始化和关闭过程删掉),如下:
int main()
{
Py_Initialize(); // 初始化 Python 解释器
vector<int> result = test_cpp2py({ 100, 200, 300, 400 });
vector<int> result_1 = test_cpp2py({ 100, 200, 300, 400 });
cout << "\n上面为python脚本内的输出,下面为C++端的输出:\n";
cout << "\n返回值为:";
for (int item : result) {
cout << item << ' ';
}
cout << endl;
for (int item : result_1) {
cout << item << ' ';
}
cout << endl;
Py_Finalize(); // 关闭 Python 解释器
}
搞定!!!