文章目录
如何在Qt中多次调用python
一、简介
在我上篇文章中,介绍了 如何在Qt中使用Python进行混编,链接为:如何在Qt中使用Python并打包发布_qt for python 如何打包文件-CSDN博客。
其中存在一个问题,就是: 无法多次调用Python函数。根据实际测试,一般情况下,当import一些特定的库时,就不能够多次调用了,例如numpy库等。
通过这篇文章,我将阐述如何解决这个问题,希望能对各位大佬有所启发。
开发环境:
Windows x64
Qt 5.12.3
二、问题描述
值得注意的是,这部分问题现象是基于我上篇文章代码实现的,但出现问题的情况均类似。
2.1 第二次调用时出现 “pModule fali”
这种情况出现在上文所写的结构中,如果第二次点击按钮将出现该报错信息。
通用结构是以下内容(相信大家跟着网上的教程都会遇到的情况):
-
将
Py_Initialize();
和Py_Finalize();
以及函数调用写在同一个函数中,例如:void pythonFunc(void) { /* 初始化 Python 解释器 */ Py_Initialize(); // 加载 python 脚本及函数调用 ...... ...... // 并销毁自上次调用Py_Initialize()以来创建并为被销毁的所有子解释器。 Py_Finalize(); }
此时,如果你第二次调用该函数,将会出现下图所示报错:
此时我们可以在代码中加入报错信息提示,根据上图我们可以找到错误出现的位置,位于 pModule
初始化的时候,则加入以下信息(示例):
// 加载 python 脚本
PyObject* pModule = PyImport_ImportModule("testQt"); // 脚本名称
if (!pModule) {
ui->textEdit->insertPlainText("[db:] pModule fail\n");
/*********** 加入以下内容 ************/
PyErr_Print();
PyErr_Clear();
/*********** 加入以下内容 ************/
return;
}
else
ui->textEdit->insertPlainText("[db:] pModule success\n");
之后我们调用的时候,在 Qt 程序输出界面会出现以下报错:
其中通过一大堆报错,我们大致可以判断,是 import np
这个库的时候出现了错误。
2.2 只能调用4次,在第5次的时候报错
一般情况下,大家通过百度,都会把 Py_Initialize();
和 Py_Finalize();
分别写在 Widget 类的构造、析构中。例如:
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
/* 初始化 Python 解释器 */
Py_Initialize();
}
Widget::~Widget()
{
// 并销毁自上次调用Py_Initialize()以来创建并为被销毁的所有子解释器。
Py_Finalize();
delete ui;
}
void pythonfunc(void)
{
// 加载 python 脚本及函数调用
......
......
}
如上所示的结构,大家就会发现只能调用4次,在第5次调用的时候就会出现错误。一般都是在调用第一个函数时出现的。我这里给出示例:
-
以下是我调用第一个函数的代码:
/*************************************** 函数调用 *******************************************/ PyObject* pFuncPrintFunc = PyObject_GetAttrString(pModule, "printFunc"); // 获取函数 printFunc - 无参、无返回值 if (!pFuncPrintFunc) { ui->textEdit->insertPlainText("[db:] pFuncPrintFunc fail\n"); PyErr_Print(); PyErr_Clear(); return; } else ui->textEdit->insertPlainText("[db:] pFuncPrintFunc success\n"); PyObject_CallFunction(pFuncPrintFunc, nullptr); // 调用函数 pFuncPrintFunc Py_DECREF(pFuncPrintFunc); // 释放指针
这里我调用了一个叫做
printFunc
的函数。并加入了 打印报错信息。 -
报错信息为:
提示我们没有这个函数。
三、解决方法
出现这种情况,经过作者本人的探究,是由于 Python GIL 全局解释器锁 导致的问题。关于 GIL 的相关知识,可以参考以下文章:
正是由于 GIL 锁的缘故,在重复调用的过程中,并没有释放该锁,所以导致了我们只能调用一次,而调用4次的情况只是巧合,实际上在第二次调用的过程中就已经出现了问题。
那么有人会问,我使用 Py_Finalize();
函数释放Python解释器环境为什么还是不行,关于这样的问题,我在上篇文章中有所解释,这里我再次粘贴出来:
图中文字来自官方文档 概述 — Python 3.11.8 文档 ,可见分配给模块的内存无法进行释放,而我们多次调用的时候,重复操作同一块内存,而由于上次操作完GIL锁没有被释放,就造成了报错的情况。
我们可以手动操作GIL,以下我给出具体的解决方法:
-
在工程中新建一个类,我这里命名为
PyThreadStateLock
,同时新建出它的文件,如下所示:实际上我们只需要
.h
文件即可,我们在.h
文件中实现效果一致。 -
在
pythreadstatelock.h
中添加以下内容:#ifndef PYTHREADSTATELOCK_H #define PYTHREADSTATELOCK_H #undef slots #include <Python.h> #define slots Q_SLOTS class PyThreadStateLock { public: PyThreadStateLock(void) { _save = nullptr; nStatus = 0; nStatus = PyGILState_Check(); // 检测当前线程是否拥有 GIL 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
至此我们就构建了自己的 GIL 操作类,不愿意这样写的朋友,可以把构造和析构写在
.cpp
文件中,效果一致。 -
仍然基于上节的 python 程序,我这里对
wiget.h
做出以下调整(大家也可以按照自己的程序来,结构基本一致,只需要按需修改成自己的函数、文件名即可):#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include "pythreadstatelock.h" namespace Ui { class Widget; } class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent = nullptr); ~Widget(); private slots: void on_PushButton_released(); private: Ui::Widget *ui; void initPy(void); void funcPy(void); }; #endif // WIDGET_H
-
对
widget.cpp
文件做出以下调整:#include "widget.h" #include "ui_widget.h" #include <QDebug> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); QString path = QCoreApplication::applicationDirPath() + "/pythonEnv/"; Py_SetPythonHome((wchar_t *)(reinterpret_cast<const wchar_t *>(path.utf16()))); initPy(); } Widget::~Widget() { PyGILState_Ensure(); // 释放 GIL 锁 // 并销毁自上次调用Py_Initialize()以来创建并为被销毁的所有子解释器。 Py_Finalize(); delete ui; } /*! * @File : widget.cpp * @Brief : Python 解释器初始化 * @Details : 对Python解释器进行初始化操作 * @Param : void * @Return : void * @Author : Liu Jiahao * @Date : 2024-02-26 17:33:15 * @Version : v1.1 * @Copyright : Copyright By Liu Jiahao, All Rights Reserved * */ void Widget::initPy() { /* 初始化 Python 解释器 */ Py_Initialize(); // 判断是否初始化成功 if (!Py_IsInitialized()) { ui->textEdit->insertPlainText("[db:] Py_Initialize fail\n"); return; } else { ui->textEdit->insertPlainText("[db:] Py_Initialize success\n"); /* 导入 sys 模块设置模块地址,以及Python脚本路径 */ PyRun_SimpleString("import sys"); // 该路径是相对 buil... 目录而言的 PyRun_SimpleString("sys.path.append('./')"); // 启动子线程前执行,为了释放 PyEval_InitThreads 获得的全局锁,否则子线程可能无法获取到全局锁 PyEval_ReleaseThread(PyThreadState_Get()); } } /*! * @File : widget.cpp * @Brief : Python 调用函数 * @Details : 用于Python的调用 * @Param : void * @Return : void * @Author : Liu Jiahao * @Date : 2024-02-26 17:50:37 * @Version : v1.1 * @Copyright : Copyright By Liu Jiahao, All Rights Reserved * */ void Widget::funcPy() { PyThreadStateLock PyThreadStateLock; // 获取全局锁 // 加载 python 脚本 PyObject* pModule = PyImport_ImportModule("testQt"); // 脚本名称 if (!pModule) { ui->textEdit->insertPlainText("[db:] pModule fail\n"); PyErr_Print(); PyErr_Clear(); return; } else ui->textEdit->insertPlainText("[db:] pModule success\n"); PyObject* pFuncPrintFunc = PyObject_GetAttrString(pModule, "randFunc"); // 获取函数 printFunc - 无参、无返回值 if (!pFuncPrintFunc) { ui->textEdit->insertPlainText("[db:] randFunc fail\n"); return; } else ui->textEdit->insertPlainText("[db:] randFunc success\n"); PyObject* ret = PyObject_CallFunction(pFuncPrintFunc, nullptr); // 调用函数 pFuncPrintFunc int a; PyArg_Parse(ret, "i", &a); ui->textEdit->insertPlainText("return value:" + QString().number(a) + "\n"); Py_DECREF(ret); Py_DECREF(pFuncPrintFunc); // 释放指针 PyObject* pFunc = PyObject_GetAttrString(pModule, "printFunc"); PyObject_CallFunction(pFunc, nullptr); Py_DECREF(pFunc); ui->textEdit->insertPlainText("\n"); Py_DECREF(pModule); } /*! * @File : widget.cpp * @Brief : 按键释放回调函数 * @Details : 相当于按键松手检测 * @Param : void * @Return : void * @Author : Liu Jiahao * @Date : 2024-02-22 16:24:14 * @Version : v1.1 * @Copyright : Copyright By Liu Jiahao, All Rights Reserved * */ void Widget::on_PushButton_released() { funcPy(); }
然后我们即可成功运行,多次调用也不再话下。这里我需要对 widget.cpp
做出以下解释:
-
在初始化的时候调用
initPy()
函数,初始化 python。其中依旧是初始化解释器操作,唯一不同的是,增加了释放 GIL 的操作函数:PyEval_ReleaseThread(PyThreadState_Get()); // 释放全局锁
-
在调用 python 文件中函数的时候,即
funcPy()
函数执行的第一句是构建我们之前写的PyThreadStateLock
类,这里申明为 局部变量,就类似于Qt多线程中的QMutex
锁一样。由于我们实现的是构造和析构,在构造PyThreadStateLock
的时候加锁,在析构的时候解锁。这是必要的!!! -
析构的过程中,除了
Py_Initialize()
函数以外,我还调用了:PyGILState_Ensure(); // 释放 GIL 锁
否则,在我们关闭窗口结束程序的时候,会报错: 程序异常结束。
综上所述,这是对于 Qt多次调用python 的解决方法实现。无法显示print的内容可以翻阅 本文 4.2 章节,其中有解决方法。
感谢以下大佬,其参考文章如下:
四、调试技巧
4.1 在Qt中获取Python报错信息
通过对本文的阅读,我相信有人发现了,就是添加以下两个函数即可:
PyErr_Print();
PyErr_Clear();
具体实现可以参考 本文中 2.1 所示内容。
4.2 在Qt中获取Python的print信息
有人会发现,使用本文的方法后,Qt程序输出界面无法获取到python print的内容,只有在程序结束时才能看到。其实,python 的 print 是有缓冲区的,而我们只需要设置缓冲区立即刷新即可,在python文件中进行修改,如下所示:
def printFunc():
'''
打印信息
:return: None
'''
print("Hello World!", flush=True)
添加参数 flush = True
,即可实现实时输出。
五、写在最后
本文介绍了 如何在Qt中多次调用python,以及显示报错信息和print的内容的相关方法。
本文中的代码已经开源,链接如下:
Qt/02.Qt中多次调用Python · 刘佳豪/CSDN_OPEN_SRC - 码云 - 开源中国 (gitee.com)
如无法点击跳转,可自行复制:
https://gitee.com/liu-jiahaohappy/CSDN_OPEN_SRC/tree/main/Qt/02.Qt中多次调用Python
欢迎广大读者提出问题以及修改意见,本人看到后会给予回应,欢迎留言。
另外,由于文章是作者手打的文字,有些地方可能文字会出错,望谅解,也可私信联系我,我对其进行更改。
-
个人CSDN账号:刘梓谦_-CSDN博客
-
GitHub:Jiahao-Liu29 (github.com)