如何在Qt中多次调用python


如何在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,以下我给出具体的解决方法:

  1. 在工程中新建一个类,我这里命名为 PyThreadStateLock,同时新建出它的文件,如下所示:

    在这里插入图片描述

    实际上我们只需要 .h 文件即可,我们在 .h 文件中实现效果一致。

  2. 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 文件中,效果一致。

  3. 仍然基于上节的 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
    
    
  4. 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 做出以下解释:

  1. 在初始化的时候调用 initPy() 函数,初始化 python。其中依旧是初始化解释器操作,唯一不同的是,增加了释放 GIL 的操作函数:

     PyEval_ReleaseThread(PyThreadState_Get());		// 释放全局锁
    
  2. 在调用 python 文件中函数的时候,即 funcPy() 函数执行的第一句是构建我们之前写的 PyThreadStateLock 类,这里申明为 局部变量,就类似于Qt多线程中的 QMutex 锁一样。由于我们实现的是构造和析构,在构造 PyThreadStateLock 的时候加锁,在析构的时候解锁。这是必要的!!!

  3. 析构的过程中,除了 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

欢迎广大读者提出问题以及修改意见,本人看到后会给予回应,欢迎留言。
另外,由于文章是作者手打的文字,有些地方可能文字会出错,望谅解,也可私信联系我,我对其进行更改。

  • 20
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值