如何解决继承QObject与logging.Handler的自定义日志处理器在程序关闭时的报错问题?
我之前也碰到过一模一样的问题!当时为了在PyQt界面里展示日志,写了个同时继承QObject和logging.Handler的自定义处理器,用pyqtSignal传递日志记录,运行起来都正常,结果每次关程序都会蹦出这个RuntimeError,查了半天才搞明白原因。
先把报错信息贴出来,就是你看到的这个:
File "C:\Program Files\Python313\Lib\logging\__init__.py", line 2242, in shutdown if getattr(h, 'flushOnClose', True): RuntimeError: wrapped C/C++ object
问题根源
这个错误本质是Python日志模块的 shutdown 流程和PyQt的对象销毁顺序冲突了:当程序关闭时,PyQt会先销毁GUI相关的C对象(也就是你的自定义处理器里继承自QObject的部分),但之后Python的logging模块才会执行shutdown操作,遍历所有已注册的处理器,尝试访问它们的flushOnClose属性——这时候你的处理器对应的C对象已经被销毁,自然就触发了RuntimeError。
几种可行的解决方案
方案1:程序退出前手动移除自定义处理器
这是最直接有效的办法,在程序关闭的时机,把你的自定义处理器从日志器里移除,这样logging shutdown的时候就不会遍历到它了。
比如你可以在主窗口的closeEvent里做这件事:
from PyQt5.QtWidgets import QMainWindow import logging class MainWindow(QMainWindow): def __init__(self): super().__init__() # 初始化你的自定义日志处理器 self.log_handler = YourCustomHandler() logging.getLogger().addHandler(self.log_handler) def closeEvent(self, event): # 移除处理器 logging.getLogger().removeHandler(self.log_handler) # 调用父类的closeEvent确保正常关闭 super().closeEvent(event)
或者也可以通过QApplication的aboutToQuit信号来绑定移除操作,适合没有主窗口的场景:
from PyQt5.QtWidgets import QApplication import sys import logging app = QApplication(sys.argv) log_handler = YourCustomHandler() logging.getLogger().addHandler(log_handler) def on_app_quit(): logging.getLogger().removeHandler(log_handler) app.aboutToQuit.connect(on_app_quit) # 运行程序 sys.exit(app.exec_())
方案2:重写自定义处理器的close方法,添加异常捕获
如果不想手动移除处理器,也可以在自定义处理器里重写close方法,给涉及PyQt对象的操作加上异常捕获,避免报错:
from PyQt5.QtCore import QObject, pyqtSignal import logging class YourCustomHandler(QObject, logging.Handler): log_signal = pyqtSignal(object) def __init__(self): QObject.__init__(self) logging.Handler.__init__(self) def emit(self, record): # 发送日志信号的逻辑 self.log_signal.emit(record) def close(self): try: # 先调用父类的close方法,如果有涉及PyQt对象的操作也放在try里 super().close() except RuntimeError: # 捕获C++对象已销毁的异常,直接忽略 pass
方案3:使用弱引用包装信号(相对复杂)
如果上面两种方法都不适用,还可以用weakref来包装信号相关的逻辑,避免持有PyQt对象的强引用,但这种方法相对繁琐,一般前两种就足够解决问题了。
总结
最推荐方案1,手动在程序退出前移除处理器,逻辑简单清晰,能从根源上避免冲突。方案2适合没法提前移除处理器的场景,用异常捕获兜底。
备注:内容来源于stack exchange,提问作者mahkitah

