最近用在Qt里调用python,遇到了很多问题,记录一下。
一开始是直接普通的调用流程:
void testPython(){
// 连接python
// 初始化python解释器
Py_Initialize();
PyObject * pModule = NULL;
PyObject * pFunc = NULL;
// 设置python文件路径
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('C:/postgraduate/LVPredictor-software/LVPredictor/')"); // 设置绝对路径
pModule = PyImport_ImportModule("predictModel");//调用的Python文件名 py文件放置exe同级
if (pModule == NULL)
{
PyErr_Print();
cout << "PyImport_ImportModule Fail!" << endl;
return;
}
// 设置调用的函数名
pFunc = PyObject_GetAttrString(pModule, "predict");
// 转换输入数据,将字典输入
PyObject* dict = PyDict_New();
for(auto data : inputData){
PyDict_SetItem(dict, Py_BuildValue("s", data.first.c_str()), Py_BuildValue("d", data.second)); // 这里要注意"s"表示c类型的字符串,所以要加c_str()否则报错
}
// 创建参数对象
PyObject* args = PyTuple_New(1);
PyTuple_SetItem(args, 0, dict); // 0表示数据dict在参数args中的位置索引
PyObject *pReturn = NULL; //返回值
pReturn = PyEval_CallObject(pFunc, args); //调用函数
// 转换返回值格式
double re;
PyArg_Parse(pReturn, "d", &re);
cout<<"result: "<<re<<endl;
Py_Finalize();//调用Py_Finalize,和Py_Initialize相对应的.
}
运行程序后发现,这个函数只能调用一次,再次重复执行的时候就会让系统崩溃。在网上查了很多资料后,说是由于Python解释器有全局解释锁GIL,导致在同一时刻只能有一个线程拥有解释器,所以在C++多线程调用python脚本时,需要控制GIL,线程获取GIL。所以一个程序里,无论有多少条线程调用python,python只能初始化一次。每次线程要调用python时,都要拥有GIL。
然后参考了以下两个博客:
https://blog.csdn.net/t15281180631/article/details/118942080
https://blog.csdn.net/weixin_42837024/article/details/100023253
对代码做了修改。
(1)新建一个PyThreadStateLock头文件
#ifndef PYTHREADSTATELOCK_H
#define PYTHREADSTATELOCK_H
//将全局解释器锁和线程的相关操作用类封装
#include "Python.h"
class PyThreadStateLock
{
public:
PyThreadStateLock(void)
{
_save = nullptr;
nStatus = 0;
nStatus = PyGILState_Check(); //检测当前线程是否拥有GIL
PyGILState_STATE gstate;
if (!nStatus)
{
gstate = PyGILState_Ensure(); //如果没有GIL,则申请获取GIL
nStatus = 1;
}
_save = PyEval_SaveThread();
PyEval_RestoreThread(_save);
}
~PyThreadStateLock(void)
{
_save = PyEval_SaveThread();
PyEval_RestoreThread(_save);
if (nStatus)
{
PyGILState_Release(gstate); //释放当前线程的GIL
}
}
private:
PyGILState_STATE gstate;
PyThreadState *_save;
int nStatus;
};
#endif // PYTHREADSTATELOCK_H
使用上面这一段代码,发现调用一次python执行完成之后总是报错“Fatal Python error: PyEval_SaveThread: NULL tstate”。我不知道是什么原因,就换成了下面这一段代码:
#ifndef PYTHREADSTATELOCK_H
#define PYTHREADSTATELOCK_H
//将全局解释器锁和线程的相关操作用类封装
#include "Python.h"
class PyThreadStateLock
{
public:
PyThreadStateLock(void){
state = PyGILState_Ensure();
}
~PyThreadStateLock(void){
PyGILState_Release(state);
}
private:
PyGILState_STATE state;
};
#endif // PYTHREADSTATELOCK_H
(2)在需要调用python的代码文件里,创建一个初始化python解释器的函数,在界面初始化的时候调用一下这个函数,我是直接在构造函数里调用的。
void PythonInit()
{
if (!Py_IsInitialized())
{
//1.初始化Python解释器,这是调用操作的第一步
Py_Initialize();
if (!Py_IsInitialized()) {
qDebug("Initial Python failed!");
// emit failed();
}
else {
//执行单句Python语句,用于给出调用模块的路径,否则将无法找到相应的调用模块
// 初始化线程支持
PyEval_InitThreads();
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('C:/postgraduate/LVPredictor-software/LVPredictor/')"); // 设置绝对路径
// 启动子线程前执行,为了释放PyEval_InitThreads获得的全局锁,否则子线程可能无法获取到全局锁。
PyEval_ReleaseThread(PyThreadState_Get());
qDebug("Initial Python Success!");
}
}
}
(3)在需要调用python的地方添加一句“class PyThreadStateLock PyThreadLock;”:
void testPython(){
class PyThreadStateLock PyThreadLock;//获取全局锁
PyObject * pModule = NULL;
PyObject * pFunc = NULL;
pModule = PyImport_ImportModule("predictModel");//调用的Python文件名 py文件放置exe同级
if (pModule == NULL)
{
PyErr_Print();
cout << "PyImport_ImportModule Fail!" << endl;
return;
}
// 设置调用的函数名
pFunc = PyObject_GetAttrString(pModule, "predict");
// 转换输入数据,将字典输入
PyObject* dict = PyDict_New();
for(auto data : inputData){
PyDict_SetItem(dict, Py_BuildValue("s", data.first.c_str()), Py_BuildValue("d", data.second)); // 这里要注意"s"表示c类型的字符串,所以要加c_str()否则报错
}
// 创建参数对象
PyObject* args = PyTuple_New(1);
PyTuple_SetItem(args, 0, dict); // 0表示数据dict在参数args中的位置索引
PyObject *pReturn = NULL; //返回值
pReturn = PyEval_CallObject(pFunc, args); //调用函数
// 转换返回值格式
double re;
PyArg_Parse(pReturn, "d", &re);
cout<<"result: "<<re<<endl;
// Py_Finalize();//注意这一句不能要,否则会运行崩溃
}
以上三步就可以了。
但是我发现我还出了一个问题,我的python代码是这样的:
# 传入参数data是一个字典
def predict(data):
x = pd.DataFrame(columns=["ID_1", "ID_2", "ID_3"])
x = x.append(data, ignore_index=True)
print(x)
print('python used!')
return 100
我在Qt里调用这个函数,发现Qt里能够获取到返回值,但是控制台却没有打印x和"python used!“。很奇怪。我以为是python函数里的语句没有执行,在网上查了很多也没查到什么解决方案。于是我将最后一句返回语句换成” return x.iloc[0][“ID_1”] ",即返回x的第一行第一列的数据(是由Qt传到python的数据),发现是能够准确返回到