使用自定义logging.Formatter为何会阻止PyQt5的QWidget对象被正常删除?
先来看这个最小可复现示例(MCVE):
import sys from PyQt5.QtWidgets import QWidget class MyWidget(QWidget): """QWidget based class to configure a Simulator object.""" def __init__(self,parent : QWidget): """ Construct a new object. param: parent : QWidget Parent class """ super().__init__(parent) print("Created GUI") def __del__(self): print("Deleting GUI") if __name__ == '__main__': from PyQt5.QtWidgets import QMainWindow, QApplication import logging class CustomFormatter(logging.Formatter): def format(self, record): formatter = logging.Formatter("%(message)s") return formatter.format(record) logger = logging.getLogger("my_logger") logger.setLevel(logging.INFO) ch = logging.StreamHandler() ch.setLevel(logging.INFO) #ch.setFormatter(CustomFormatter()) logger.addHandler(ch) app = QApplication(sys.argv) wnd = QMainWindow() widget = MyWidget(wnd) wnd.setCentralWidget( widget ) wnd.show() sys.exit(app.exec())
正常情况下(注释掉ch.setFormatter(CustomFormatter())),打开GUI后点击关闭按钮,输出如下:
Created GUI Deleting GUI
但取消注释ch.setFormatter(CustomFormatter())后,重复操作只会输出Created GUI,Deleting GUI消失——也就是MyWidget.__del__完全没被调用。
核心原因:__del__特性 + 全局引用引发的循环引用无法回收
1. Qt父子机制 vs Python垃圾回收的正常流程
在PyQt中,widget的父对象是wnd,Qt的父子对象机制会保证:当父窗口wnd被销毁时,会自动销毁子控件widget,这时候widget的引用计数直接降到0,触发__del__方法输出日志。
2. 自定义Formatter引入的全局引用问题
启用CustomFormatter后,这个格式化器实例会被StreamHandler持有,而StreamHandler又被添加到全局的logger单例(logging.getLogger返回的是全局单例,属于logging模块的全局状态)。程序退出时,Python的全局对象销毁顺序不确定,logger及其关联的StreamHandler、CustomFormatter会存活到程序后期,干扰Qt的对象销毁流程。
3. __del__让循环引用无法被处理
MyWidget和wnd本身就存在循环引用:widget.parent() → wnd,wnd.centralWidget() → widget。正常情况下Qt的主动销毁机制会绕过Python垃圾回收,直接销毁对象;但当全局logger引用存在时,Qt的销毁逻辑被延迟,循环引用被留给Python垃圾回收器处理。
而Python垃圾回收器无法自动清理带有__del__方法的循环引用对象——因为无法确定循环中对象的__del__调用顺序,会把这些对象标记为“不可达但无法清理”,直接跳过__del__的调用。
验证与解决方法
1. 验证:手动清理全局引用并触发回收
在sys.exit(app.exec())之前添加以下代码,手动清理全局引用并触发垃圾回收,就能看到Deleting GUI正常输出:
# 在sys.exit之前插入 logger.removeHandler(ch) del ch del logger del widget del wnd import gc gc.collect()
2. 推荐方案:弃用__del__,改用Qt原生机制
Python的__del__调用时机本身就不可靠,在PyQt中更推荐使用Qt的destroyed信号处理销毁逻辑,完全不受Python垃圾回收的影响:
class MyWidget(QWidget): def __init__(self, parent: QWidget): super().__init__(parent) print("Created GUI") # 绑定destroyed信号 self.destroyed.connect(self.on_destroyed) def on_destroyed(self): print("Deleting GUI")
只要Qt销毁对象,这个信号就会触发,无论是否有全局引用干扰。
备注:内容来源于stack exchange,提问作者jpo38

