从 PyQt5 窗口闪退问题看 Python 垃圾回收与消息机制

前言

此篇文章源于知乎上的一个问题,使用 PyQt5 编写 GUI 程序时,新创建的界面会闪退,本篇文章仅作记录以防以后忘记。

问题代码

python 复制代码
import sys

from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton


class Main(QMainWindow):
    def __init__(self):
        super(Main, self).__init__()
        self.button = QPushButton('test', self)
        self.button.clicked.connect(self.start)
        self.show()

    def start(self):
        w = QMainWindow()
        w.show()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w1 = Main()
    sys.exit(app.exec_())

在运行以上代码时,会显示一个简单的界面,界面上有一个按钮,如下图:

当点击 test 按钮时,本应该弹出一个新的窗口,但实际情况却是,有一个窗口一闪而过,重复点击新的窗口仍然一闪而过,快到我根本无法截到窗口打开的样子。

看到这里,大家可以猜测下为什么会出现这种情况,可以从变量作用域方面来猜。

问题原因

下面公布答案,关于窗口闪退的原因其实很简单,那就是 w 变量被回收了。

w 变量的作用范围只在 start 函数内,start 函数运行结束后函数内的所有变量都会被垃圾回收掉,垃圾回收时会自动执行对象中对应的 __del__ 函数(又被叫做析构函数),之后对象被销毁,像没来过一样。

了解了以上知识后来解释窗口闪退的原因就很方便了,w 对象是一个窗口对象,它被创建后紧接着调用了 show 方法,show 方法会将窗口显示在屏幕上,之后 start 函数执行完毕,解释器要清理掉函数作用域内所有的局部变量,自动执行它们的 __del__ 函数,这时窗口就会被回收掉,所以窗口就会自动关闭了。

由于处理器执行的速度非常快,我上面描述的流程在一瞬间就执行结束了,所以在我们看来窗口就像是"闪退" 了,实际上它只是存在的时间太短了而已。

让我们添加一个延时函数再来看看结果:

python 复制代码
import sys
import time

from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton


class Main(QMainWindow):
    def __init__(self):
        super(Main, self).__init__()
        self.button = QPushButton('test', self)
        self.button.clicked.connect(self.start)
        self.show()

    def start(self):
        w = QMainWindow()
        w.show()
        time.sleep(3)
        print('start end!')


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w1 = Main()
    sys.exit(app.exec_())

执行以上代码,会发现可以正常打开第二个窗口了,但是在三秒过后窗口仍然自己关闭了,那是因为延时时间到了解释器开始了清理工作,窗口对象就被清理掉了。

而且在这三秒钟内我们会发现窗口不响应我们对窗口发出的任何动作,包括最大化、最小化和关闭按钮统统无效,这是 Windows 消息机制的原因。test 按钮绑定了事件处理函数 start,在点击 test 按钮后,按钮单击消息会被发出,放到消息队列中, 消息处理函数依次处理消息队列中的消息,当处理到 start 函数时,会遇到 sleep 函数,此时消息处理函数会等待,等待的过程中也就无法处理其他的消息,导致界面无响应,也就无法处理我们发出的任何消息(消息会被阻塞住,直到消息处理函数恢复正常)。

注:PyQt5 中有一个新的概念,将消息机制抽象为信号和槽,通过信号和槽绑定的方式来传递信号(消息),这样一来方便了后台线程和前台界面进行交互,这种抽象其实大大简化了我们的理解难度。

问题解决

理解了原理后修复这个问题就变得很容易了,既然函数结束变量会被回收,那么将窗口对象绑定到 self 上就可以了,self 会在整个程序退出时才会被销毁,这样就可以防止新打开的窗口闪退了。

python 复制代码
import sys

from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton


class Main(QMainWindow):
    def __init__(self):
        super(Main, self).__init__()
        self.button = QPushButton('test', self)
        self.button.clicked.connect(self.start)
        self.show()
        self.w = None

    def start(self):
        self.w = QMainWindow()
        self.w.show()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w1 = Main()
    sys.exit(app.exec_())

问题验证

问题已经解决了,下面来验证下 start 函数执行完毕解释器是怎么销毁变量的,根据上面的结论我们知道,解释器在垃圾回收时会调用对象对应的 __del__ 函数,那么我们重写 QMainWindow 类的 __del__ 函数,观察是否会调用到我们重写的 __del__ 函数。

python 复制代码
import sys

from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton


class M(QMainWindow):
    def __del__(self):
        print('call __del__')


class Main(QMainWindow):
    def __init__(self):
        super(Main, self).__init__()
        self.button = QPushButton('test', self)
        self.button.clicked.connect(self.start)
        self.show()
        self.w = None

    def start(self):
        w = M()
        w.show()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w1 = Main()
    sys.exit(app.exec_())

运行以上代码,我们发现,窗口闪退时,call __del__ 被打印出来了,解释器调用了我们重写后的 __del__ 函数,说明窗口对象的确是被 Python 解释器销毁了。

总结

在本次解决问题的过程中,我们可以学到 __del__ 函数的用法,还了解了 Windows 的消息机制,说明多踩坑还是可以学到不少东西的,哈哈哈🤣

参考链接

docs.python.org/zh-cn/3.8/r...del #object.del

相关推荐
凤山老林39 分钟前
Spring Boot 接入国密 SM2/SM3/SM4 的一些实践记录
spring boot·后端·python
伯恩bourne2 小时前
Spring Boot Bean 注册的两种方式:@Component 家族 vs @Configuration + @Bean
java·spring boot·后端
张彦峰ZYF2 小时前
停顿短不等于延迟低:G1、ZGC 与低延迟服务的目标分野、代价账本与选型标定
后端·回收暂停、应用停止总时长·到达安全点、分配停滞·屏障吃吞吐、并发线程吃核心·并发窗口吃堆余量·分区式靠挑回收集兑现目标·染色指针式不支持压缩引用
pride.li2 小时前
第03章 Python 教程-基础语法:注释与变量
服务器·前端·后端·python·算法·计算机视觉
upala3 小时前
【仓颉语言入门 · 第二篇 第 6 课】
后端
罗丹丹Luna3 小时前
移动端显示“已提交”以后:SAP 与低代码集成该怎样暴露真实状态
后端·架构
码事漫谈3 小时前
因为太想讨好你,所以AI会骗你
后端
吴凡wudianmei3 小时前
每调一次模型都花钱?如何用工程手段把调用压到最少?
前端·后端·架构
花间相见3 小时前
【计算基础|MySQL01】【上手】安装连接、数据类型与建库建表
数据库·后端
花间相见3 小时前
【计算基础|MySQL02】【上手】SQL 增删改查
数据库·后端