前言:在之前做的一个项目中,要使用一段Python的代码。一般来讲可以将Python代码中的功能在C++项目中重构,但是如果Python项目太大,或者这部分是别人写的,自己不清楚整个项目的逻辑,这样重构起来就比较麻烦。这里给出了另外一种实现方法,即利用Python的API使得C++项目可以直接启动Python程序,快速在PC端验证代码功能。
急性子可直接看:2.2 C++调用python有参有返回值函数示例(简单项目示例)
目录
2.1.1Py_Initialize()函数,初始化Python环境
2.1.2PyRun_SimpleString()函数,执行字符串命令
2.1.3PyImport_ImportModule()函数,导入Python模块
2.1.4PyObject_GetAttrString()函数,选择要调用的python函数名
2.1.5PyTuple_New()和PyTuple_SetItem()函数,创建一个元组对象以及设置元组对象
2.1.6PyObject_CallObject(pFunc, pArgs)函数,调用py函数
2.1.7PyArg_Parse()以及PyTuple_GetItem()函数,用于解析Python函数返回值
2.1.8PyLong_AsLong()函数,将Python对象转化为Long型数据
2.2 C++调用python有参有返回值函数示例(简单项目示例)
一、设置C++项目环境
首先,想要调用Python程序,在本机肯定需要Python的运行环境,有的会安装Anaconda配有多个Python环境,这里最好选择和你Python项目相匹配的环境,以避免缺少必要文件。下面以VisualStudio2019举例,来设置Python环境运行路径。注意:要根据你Python下载的版本来选择你的C++项目是Debug的还是Release的,一般来讲,都是Release的。
1.1 VS2019环境设置
头文件包含目录:
因为我的Python项目的环境是存在于Anaconda中的,所以首先需要将Anaconda中Python运行时所需要的头文件包含进来。在VC++目录->包含目录这个选项添加路径,具体路径为:
D:\Anaconda(你自己的Anaconda的安装位置)\envs\MyObject(你自己创建的环境目录)\include
注意替换上方括号中的内容,将地址换成自己的地址。
注意,如果你Python项目中包含了Numpy或者其他额外文件,还需要额外添加相应的目录,例如我的Python中添加了Numpy,则我要才VC++目录中额外包含下面的路径:
D:\Anaconda\envs\MyObject\Lib\site-packages\numpy\core\include\numpy
包含库目录:
头文件包含完毕后注意包含库文件,这里指的是Python运行的动态库文件。在VC++目录->库目录下包含该文件,路径为:
D:\Anaconda\envs\MyObject\libs
包含完毕后还要在链接器->输入->附加依赖项输入python36.lib(这里的动态库名称要和你之前libs目录下的文件名称相同)。
二、C++调用Python文件的方法
2.1常见API
2.1.1Py_Initialize()函数,初始化Python环境
Py_Initialize()函数是Python C语言API中的一个函数,它用于初始化Python解释器的运行环境。在使用Python C扩展开发时,通常需要在C/C++代码中调用Py_Initialize()函数来初始化Python解释器的环境,确保正确的Python解释器状态。
一般来讲,调用该函数后还要调用Py_IsInitialized()函数来判断Python环境是否被正确初始化。示例代码如下:
Py_Initialize();//使用python之前,要调用Py_Initialize();这个函数进行初始化
if (!Py_IsInitialized())//判断环境是否初始化成功
{
printf("初始化失败!");
return 0;
}
2.1.2PyRun_SimpleString()函数,执行字符串命令
用于在C代码中执行简单的Python代码字符串。当使用Python C扩展开发时,可以使用PyRun_SimpleString()函数来执行一行或多行的Python代码。当调用这个函数时,Python会首先执行一次该函数参数的命令。一般而言,我们需要利用该函数设置Python运行环境依赖命令。例如:
PyRun_SimpleString("import sys");//相当于在Python内部执行import sys
const char* str2 = "D:/VS Code/test/call_python/x64/Release/call_python" ;
//这里设置Python文件运行的路径
char pythonCode[256] ;
_snprintf_s(pythonCode, sizeof(pythonCode), "import sys; sys.path.append('%s')", str2);
//将上面的路径拷贝到pythoncode字符串
PyRun_SimpleString(pythonCode);//在Python中执行pythonCode字符串代表的命令,即加载环境
2.1.3PyImport_ImportModule()函数,导入Python模块
PyImport_ImportModule()函数是Python C语言API提供的一个函数,用于在C代码中导入并加载一个Python模块。通过调用这个函数,可以在C/C++代码中动态地载入一个Python模块,并返回对该模块的引用。
具体来说,PyImport_ImportModule()函数完成了以下主要任务:
- 接受一个字符串参数,参数为要导入的Python模块的名称。
- 在Python解释器中搜索、导入并加载指定的Python模块。
- 返回一个指向导入的模块对象的引用,供后续在C代码中使用这个模块。
你的Python项目也许有多个py文件,这里导入你需要调用执行的py文件名称。例如:
PyObject* pModule = PyImport_ImportModule("callpython");
//这里是要调用的文件名hello.py;
if (pModule == NULL)//如果函数执行失败,则返回NULL
{
cout << "没找到该Python文件" << endl;
return 0;
}
因为我需要调用的Python文件为callpython.py所以该函数参数要输入callpython.py。同时,该函数返回一个Python对象,在Python的C API中,所有Python对象都要用pyObject类型接收,这也是C和Python的一个接口类型。
2.1.4PyObject_GetAttrString()函数,选择要调用的python函数名
PyObject_GetAttrString()函数是Python C语言API中提供的一个函数,用于获取指定Python对象的指定属性。该函数的作用是在C代码中获取Python对象的属性值。具体来说,PyObject_GetAttrString()函数的作用包括以下几个方面:
- 接受两个参数:一个是表示Python对象的指针,另一个是一个表示属性名称的C字符串。
- 在Python对象中查找并获取指定名称的属性的值。
- 返回表示获取的属性值的新的Python对象。
简而言之,我们需要利用该函数来确定我们要执行py文件对象(通过上一个函数获取的返回值)的具体的py函数名,例如:
PyObject* pFunc = NULL;// 声明一个py对象,用于接收要执行的py函数
pFunc = PyObject_GetAttrString(pModule, "test_py");
//参数一:pModule为PyImport_ImportModule返回的对象
//参数二:为要执行的py函数名称
//返回值为该函数对象,后续会直接用到该对象,用于启动Python对应名称的函数
2.1.5PyTuple_New()和PyTuple_SetItem()函数,创建一个元组对象以及设置元组对象
PyTuple_New()函数是Python C语言API中提供的一个函数,用于创建一个新的空元组对象(tuple)。具体来说,PyTuple_New()函数的功能包括以下几个方面:
- 接受一个整数参数n,表示要创建的元组的长度。
- 创建一个包含n个元素的空元组对象。
- 返回指向新创建的元组对象的指针。
PyTuple_SetItem()函数是Python C语言API提供的一个函数,用于设置元组(tuple)对象中指定位置的元素。具体来说,PyTuple_SetItem()函数的作用包括以下几个方面:
- 接受一个表示元组对象的指针和一个整数索引位置以及一个表示要设置的Python对象的指针作为参数。
- 将指定位置的元素设置为给定的Python对象。
- 在成功设置元素的情况下返回0;在出现错误时返回-1。
这两个函数常常结合起来使用,用于创建并初始化一个元组对象,用来实现C++给Python函数传递参数。如果你要执行的Python函数不需要参数则无需用到该函数,但一般来说,我们都是希望C++和Python之间存在参数传递的,所以,这里会以有参函数举例故需要用到该函数。例如,我要通过C++向test_py函数传递三个整形函数,可以如下操作:
PyObject* pArgs = PyTuple_New(3);//设置一个空的元组对象
//Py_BuildValue函数用于根据指定的格式字符串构建一个新的Python对象。
//这个函数可以在C代码中创建各种类型的Python对象,并根据给定的格式字符串初始化对象的值。
PyTuple_SetItem(pArgs, 0, Py_BuildValue("i", 1));
//将元组对象的第一个元素设置为一个整形的1
PyTuple_SetItem(pArgs, 1, Py_BuildValue("i", 2));
//将元组对象的第二个元素设置为一个整形的2
PyTuple_SetItem(pArgs, 2, Py_BuildValue("i", 3));
//将元组对象的第三个元素设置为一个整形的3
这样我们就得到了一个元组对象pArgs,该对象记录了要传递给test_py函数的参数,根据上面代码,我们知道要传递的参数为整形的1,2,3。
2.1.6PyObject_CallObject(pFunc, pArgs)函数,调用py函数
PyObject_CallObject(pFunc, pArgs) 用于调用一个 Python 可调用对象并传递参数。具体来说,这个函数的作用包括以下几个方面:
- 接受两个参数,第一个参数
pFunc
是指向要调用的 Python 可调用对象的指针,第二个参数pArgs
是要传递给该可调用对象的参数。 - 调用指定的可调用对象,传递参数作为函数调用的参数。
- 返回表示调用结果的新 Python 对象。
可以通过该函数来执行pFunc对象(即test_py函数),并将pArgs作为参数传递给该函数,具体例程如下:
PyObject* pValue = PyObject_CallObject(pFunc, pArgs);
//参数1:py函数对象
//参数2:传递给py函数的参数,即上面讲到的元组对象
//返回值:为py函数的返回值,它是一个C API定义的Python对象
通过上面代码,我们即可执行Python文件中对应的函数,并且得到其返回值存储在pValue中,但是该返回值是一个API定义的python对象,也是一个元组,不能直接使用,需要利用另外一个函数来解析它。
2.1.7PyArg_Parse()以及PyTuple_GetItem()函数,用于解析Python函数返回值
PyArg_Parse()是Python C语言扩展中用于解析Python函数参数的函数。它允许在C函数中解析传递给Python函数的参数,并将这些参数转换成C语言对应的数据类型。以下是对PyArg_Parse()的详细解释及用法说明:
//函数原型:
int PyArg_Parse(PyObject *args, const char *format, ...)
//args: 是一个Python对象,通常是传递给C扩展函数的参数元组。
//format: 是一个格式化字符串,指定了参数应该被解析成的C语言数据类型。
//...: 表示接收参数的变量。在PyArg_Parse()函数的调用中,根据格式化字符串,需将对应的C变量的地址
// 传递给PyArg_Parse(),以便该函数将解析后的值存储到相应的变量中。
/*下面是一些常见的格式化字符串及对应的C语言数据类型转换:
s: 字符串 (char*)
i: 整数 (int)
l: 长整数 (long)
f: 浮点数 (float)
d: 双精度浮点数 (double)
O: 任何Python对象 (PyObject*)
*/
该函数适用于解析函数只有一个返回值的情况,使用示例如下:
PyObject* pValue = PyObject_CallObject(pFunc, pArgs);
//pValue保存python函数返回值
int res = 0;
PyArg_Parse(pValue, "i", &res);//转换返回类型
//将pValue转换为int类型存储在res中
可以根据函数的第二个参数合理选择要转换的类型,注意:PyArg_Parse()函数只适用于解析只有一个返回值的情况。如果Python有多个返回值(Python允许函数有多个返回值),请用下面这个PyTuple_GetItem()函数。
PyTuple_GetItem()函数用于获取元组(tuple)对象中指定位置的元素。PyTuple_GetItem()函数的功能包括以下几个方面:
- 接受一个表示元组对象的指针和一个整数索引位置作为参数。
- 返回指定位置的元素对象的指针。
- 如果索引位置超出了元组的范围,函数会返回NULL,并且可能引发 IndexError 异常。
该函数常用来解析python函数的返回值(针对多返回值情况),例如:
PyObject* returnvalue = PyTuple_GetItem(pValue, 0);
//获取元组第一个元素的返回值
//参数1:元组对象
//参数2:元素对象索引
这里要补充一点的是,该函数仅适用于Python函数有多个返回值的情况,我们可以根据该函数的第二个参数将他解析出来。调该函数之后,返回值仍然是一个PyObject类型,我们还需要对它进行进一步解析。
2.1.8PyLong_AsLong()函数,将Python对象转化为Long型数据
PyLong_AsLong()函数用于将一个Python长整型对象(PyLong
对象)转换为C语言中的long
类型的整数。具体来说,PyLong_AsLong()函数的功能包括以下几个方面:
- 接受一个表示Python长整型对象的指针作为参数。
- 将该长整型对象表示的数值转换为C语言中的
long
类型的整数,并返回该整数值。 - 在转换过程中,如果Python长整型对象表示的数值超出了
long
类型所能表示的范围,会产生溢出。
与此函数功能相同的还有_PyLong_AsInt、PyObject_AsCharBuffer、PyUnicode_AsUTF8等等,具体转换解析函数可以查阅官方文档,这里只以PyLong_AsLong举例子。代码如下:
long intValue = PyLong_AsLong(returnvalue);
//将returnvalue这个python对象转化成long形数据
至此,我们便可以调用Python代码并获取对应函数的返回值实现C++和Python的简单数据传递。
2.1.9Py_XDECREF()以及Py_Finalize()函数,清理Python对象
Py_XDECREF()用于减少Python对象的引用计数。它的功能包括以下几个方面:
- 接受一个表示Python对象的指针作为参数。
- 减少该对象的引用计数,如果引用计数变为0,则该对象会被销毁并释放其所占用的内存。
- 如果对象的引用计数不为0,则函数会将引用计数减1,但不会立即释放内存。
Py_Finalize()函数用于关闭Python解释器并清理Python解释器相关的资源。它的主要作用包括以下几个方面:
- 终止Python解释器的运行并释放相关资源,包括已加载的模块、全局解释器锁(GIL)等。
- 在调用Py_Finalize()之后,Python解释器将无法再执行Python代码,并且所有使用Python解释器的资源将被释放。
- 一般情况下,当不再需要使用Python解释器时,应该调用Py_Finalize()函数来正确关闭解释器并释放资源。
对于上面的种种操作,我们都创建了大量的Python的对象,它们的数据类型基本都为PyObject,这些对象在使用完毕后最好清理掉,否则会占用大量空间,清理这些对象的函数即为Py_XDECREF,当项目完成对Python的调用后,要使用Py_Finalize函数来清理掉Python运行空间下面是一个示例:
Py_XDECREF(pModule);
Py_XDECREF(pFunc);
Py_DECREF(pValue);
...
Py_Finalize();//调用Py_Finalize,清理Python环境
//通过这些操作清理掉py对象,释放内存
2.2 C++调用python有参有返回值函数示例(简单项目示例)
根据上面的主要API,我们可以在这里完成C++调用Python的简单测试。
2.2.1Python端代码
在这里仅定义两个函数,功能一致,都用于完成三个参数的和,只不过返回值数量不一样,用来测试C++端解析函数的正确性。Python端代码如下:(文件名为hello.py,放在C++工程目录.../x64/Release 下)
def test1_py(a, b, c):
result = a + b + c
return result,a,b
def test2_py(a,b,c):
result = a+b+c
return result
2.2.2C++项目代码
在C++工程中,首先要完成环境的配置,在前面已经有介绍了,这里不重复。下面直接贴代码:
#include <Python.h>
#include<iostream>
//#include<arrayobject.h>
//根据情况选择,该C++文件可以不包含此头文件
#include<string>
using namespace std;
int main()
{
const wchar_t* str = L"D://Anaconda//envs//TF2_HLH";
Py_Initialize();//使用python之前,要调用Py_Initialize();
if (!Py_IsInitialized())
{
printf("初始化失败!");
return 0;
}
else {
PyRun_SimpleString("import sys");
const char* str2 = "D:/VS Code/test/call_python/x64/Release";
char pythonCode[256];
_snprintf_s(pythonCode,
sizeof(pythonCode),
"import sys; sys.path.append('%s')",
str2);
cout << PyRun_SimpleString(pythonCode) << endl;//这一步很重要,修改Python路径
//函数成功执行返回0
PyObject* pModule = PyImport_ImportModule("hello");//这里是要调用的文件名
PyObject* pFunc1 = NULL;
PyObject* pFunc2 = NULL;
if (pModule == NULL)
{
cout << "没找到该Python文件" << endl;
}
else {
pFunc1 = PyObject_GetAttrString(pModule, "test1_py");//将调用test1_py
pFunc2 = PyObject_GetAttrString(pModule, "test2_py");//调用函数test2_py
PyObject* pArgs = PyTuple_New(3);//创建传给Python函数的参数
PyTuple_SetItem(pArgs, 0, Py_BuildValue("i", 1));
PyTuple_SetItem(pArgs, 1, Py_BuildValue("i", 2));
PyTuple_SetItem(pArgs, 2, Py_BuildValue("i", 3));
//设置传递给Python的参数,分别为整形的(int)1,2,3
PyObject* pValue1 = PyObject_CallObject(pFunc1, pArgs);//执行了对应的函数
PyObject* pValue2 = PyObject_CallObject(pFunc2, pArgs);//执行了对应的函数
//pValue1 ,pValue2分别为两个函数的返回值
//test1_py为多返回值,test2_py为一个返回值
PyObject* returnvalue1 = PyTuple_GetItem(pValue1, 0);
PyObject* returnvalue2 = PyTuple_GetItem(pValue1, 1);
PyObject* returnvalue3 = PyTuple_GetItem(pValue1, 2);
int result1 = 0 , result2 = 0 , result3 = 0;
result1 =_PyLong_AsInt(returnvalue1);
result2 =_PyLong_AsInt(returnvalue2);
result3 =_PyLong_AsInt(returnvalue3);
cout<<"test1_py函数的返回值为:"<<result1<<" "
<<result2<<" "
<<result3<<endl;
int res = 0;
PyArg_Parse(pValue2, "i", &res);//转换返回类型
cout << "test2_py函数的返回值为:"<<res<< endl;
}
}
return 0;
}
运行该文件,第一次肯定会报错,因为此时根本没有x64文件夹,也不会有Release文件夹。报错完后,将Python文件拷贝到x64/Release目录下,如果有相关的文件依赖也要拷贝过去(在本项目中不存在)。再次运行文件,我们可以看到如下输出:
这与我们预期输出一致,至此,便完成了C++对Python的简单调用。
三、后续
上面只是C++对Python文件的简单调用,由于Python函数功能以及返回值太简单,可能上述案例并不太能够满足大家需求,后续还会持续更新,主要内容会有:
C++对Python的并发调用
C++和Python传递图像类型数据
C++调用Python实现深度学习模型预测