You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用自定义logging.Formatter为何会阻止PyQt5的QWidget对象被正常删除?

使用自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 14:43:05