前言:在之前做的一个项目中,要使用一段Python的代码。一般来讲可以将Python代码中的功能在C++项目中重构,但是如果Python项目太大,或者这部分是别人写的,自己不清楚整个项目的逻辑,这样重构起来就比较麻烦。这里给出了另外一种实现方法,即利用Python的API使得C++项目可以直接启动Python程序,快速在PC端验证代码功能。
急性子可直接看:2.2 C++调用python有参有返回值函数示例(简单项目示例)
目录
一、设置C++项目环境
1.1 VS2019环境设置
二、C++调用Python文件的方法
2.1常见API
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有参有返回值函数示例(简单项目示例)
2.2.1Python端代码
2.2.2C++项目代码
三、后续
首先,想要调用Python程序,在本机肯定需要Python的运行环境,有的会安装Anaconda配有多个Python环境,这里最好选择和你Python项目相匹配的环境,以避免缺少必要文件。下面以VisualStudio2019举例,来设置Python环境运行路径。注意:要根据你Python下载的版本来选择你的C++项目是Debug的还是Release的,一般来讲,都是Release的。
头文件包含目录:
因为我的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目录下的文件名称相同)。
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;
}
用于在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字符串代表的命令,即加载环境
PyImport_ImportModule()函数是Python C语言API提供的一个函数,用于在C代码中导入并加载一个Python模块。通过调用这个函数,可以在C/C++代码中动态地载入一个Python模块,并返回对该模块的引用。
具体来说,PyImport_ImportModule()函数完成了以下主要任务:
你的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的一个接口类型。
PyObject_GetAttrString()函数是Python C语言API中提供的一个函数,用于获取指定Python对象的指定属性。该函数的作用是在C代码中获取Python对象的属性值。具体来说,PyObject_GetAttrString()函数的作用包括以下几个方面:
简而言之,我们需要利用该函数来确定我们要执行py文件对象(通过上一个函数获取的返回值)的具体的py函数名,例如:
PyObject* pFunc = NULL;// 声明一个py对象,用于接收要执行的py函数
pFunc = PyObject_GetAttrString(pModule, "test_py");
//参数一:pModule为PyImport_ImportModule返回的对象
//参数二:为要执行的py函数名称
//返回值为该函数对象,后续会直接用到该对象,用于启动Python对应名称的函数
PyTuple_New()函数是Python C语言API中提供的一个函数,用于创建一个新的空元组对象(tuple)。具体来说,PyTuple_New()函数的功能包括以下几个方面:
PyTuple_SetItem()函数是Python C语言API提供的一个函数,用于设置元组(tuple)对象中指定位置的元素。具体来说,PyTuple_SetItem()函数的作用包括以下几个方面:
这两个函数常常结合起来使用,用于创建并初始化一个元组对象,用来实现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。
PyObject_CallObject(pFunc, pArgs) 用于调用一个 Python 可调用对象并传递参数。具体来说,这个函数的作用包括以下几个方面:
pFunc
是指向要调用的 Python 可调用对象的指针,第二个参数 pArgs
是要传递给该可调用对象的参数。可以通过该函数来执行pFunc对象(即test_py函数),并将pArgs作为参数传递给该函数,具体例程如下:
PyObject* pValue = PyObject_CallObject(pFunc, pArgs);
//参数1:py函数对象
//参数2:传递给py函数的参数,即上面讲到的元组对象
//返回值:为py函数的返回值,它是一个C API定义的Python对象
通过上面代码,我们即可执行Python文件中对应的函数,并且得到其返回值存储在pValue中,但是该返回值是一个API定义的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()函数的功能包括以下几个方面:
该函数常用来解析python函数的返回值(针对多返回值情况),例如:
PyObject* returnvalue = PyTuple_GetItem(pValue, 0);
//获取元组第一个元素的返回值
//参数1:元组对象
//参数2:元素对象索引
这里要补充一点的是,该函数仅适用于Python函数有多个返回值的情况,我们可以根据该函数的第二个参数将他解析出来。调该函数之后,返回值仍然是一个PyObject类型,我们还需要对它进行进一步解析。
PyLong_AsLong()函数用于将一个Python长整型对象(PyLong
对象)转换为C语言中的long
类型的整数。具体来说,PyLong_AsLong()函数的功能包括以下几个方面:
long
类型的整数,并返回该整数值。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对象的引用计数。它的功能包括以下几个方面:
Py_Finalize()函数用于关闭Python解释器并清理Python解释器相关的资源。它的主要作用包括以下几个方面:
对于上面的种种操作,我们都创建了大量的Python的对象,它们的数据类型基本都为PyObject,这些对象在使用完毕后最好清理掉,否则会占用大量空间,清理这些对象的函数即为Py_XDECREF,当项目完成对Python的调用后,要使用Py_Finalize函数来清理掉Python运行空间下面是一个示例:
Py_XDECREF(pModule);
Py_XDECREF(pFunc);
Py_DECREF(pValue);
...
Py_Finalize();//调用Py_Finalize,清理Python环境
//通过这些操作清理掉py对象,释放内存
根据上面的主要API,我们可以在这里完成C++调用Python的简单测试。
在这里仅定义两个函数,功能一致,都用于完成三个参数的和,只不过返回值数量不一样,用来测试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
在C++工程中,首先要完成环境的配置,在前面已经有介绍了,这里不重复。下面直接贴代码:
#include
#include
//#include
//根据情况选择,该C++文件可以不包含此头文件
#include
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函数的返回值为:"<
运行该文件,第一次肯定会报错,因为此时根本没有x64文件夹,也不会有Release文件夹。报错完后,将Python文件拷贝到x64/Release目录下,如果有相关的文件依赖也要拷贝过去(在本项目中不存在)。再次运行文件,我们可以看到如下输出:
这与我们预期输出一致,至此,便完成了C++对Python的简单调用。
上面只是C++对Python文件的简单调用,由于Python函数功能以及返回值太简单,可能上述案例并不太能够满足大家需求,后续还会持续更新,主要内容会有:
C++对Python的并发调用
C++和Python传递图像类型数据
C++调用Python实现深度学习模型预测