【发布时间】:2014-09-29 01:58:15
【问题描述】:
tl;dr -- 在 PySide 应用程序中,其方法引发异常的对象将保持活动状态,即使所有其他引用都已被删除。为什么?如果有的话,应该怎么做呢?
在使用带有 PySide GUI 的 Model-View-Presenter 架构构建一个简单的 CRUDish 应用程序的过程中,我发现了一些奇怪的行为。就我而言:
- 界面分为多个视图 - 即,显示数据不同方面的每个标签页可能是其自己的视图类
- 首先实例化视图,并在初始化时实例化自己的 Presenter,并保持对其的正常引用
- Presenter 收到对其驱动的 View 的引用,但将其存储为弱引用 (
weakref.ref) 以避免循环 - 不存在对 Presenter 的其他强引用。 (演示者可以与
pypubsub消息库间接通信,但这也只存储对侦听器的弱引用,并且不是下面 MCVE 中的一个因素。) - 因此,在正常操作中,当一个 View 被删除(例如,当一个选项卡被关闭时),它的 Presenter 被随后删除,因为它的引用计数变为 0
但是,其方法已引发异常的 Presenter 不会按预期删除。应用程序继续运行,因为 PySide 使用some magic 来捕获异常。有问题的 Presenter 继续接收和响应绑定到它的任何 View 事件。但是当 View 被删除时,抛出异常的 Presenter 会一直保持活动状态,直到整个应用程序关闭。一个 MCVE (link for readability):
import logging
import sys
import weakref
from PySide import QtGui
class InnerPresenter:
def __init__(self, view):
self._view = weakref.ref(view)
self.logger = logging.getLogger('InnerPresenter')
self.logger.debug('Initializing InnerPresenter (id:%s)' % id(self))
def __del__(self):
self.logger.debug('Deleting InnerPresenter (id:%s)' % id(self))
@property
def view(self):
return self._view()
def on_alert(self):
self.view.show_alert()
def on_raise_exception(self):
raise Exception('From InnerPresenter (id:%s)' % id(self))
class OuterView(QtGui.QMainWindow):
def __init__(self, *args, **kwargs):
super(OuterView, self).__init__(*args, **kwargs)
self.logger = logging.getLogger('OuterView')
# Menus
menu_bar = self.menuBar()
test_menu = menu_bar.addMenu('&Test')
self.open_action = QtGui.QAction('&Open inner', self, triggered=self.on_open, enabled=True)
test_menu.addAction(self.open_action)
self.close_action = QtGui.QAction('&Close inner', self, triggered=self.on_close, enabled=False)
test_menu.addAction(self.close_action)
def closeEvent(self, event, *args, **kwargs):
self.logger.debug('Exiting application')
event.accept()
def on_open(self):
self.setCentralWidget(InnerView(self))
self.open_action.setEnabled(False)
self.close_action.setEnabled(True)
def on_close(self):
self.setCentralWidget(None)
self.open_action.setEnabled(True)
self.close_action.setEnabled(False)
class InnerView(QtGui.QWidget):
def __init__(self, *args, **kwargs):
super(InnerView, self).__init__(*args, **kwargs)
self.logger = logging.getLogger('InnerView')
self.logger.debug('Initializing InnerView (id:%s)' % id(self))
self.presenter = InnerPresenter(self)
# Layout
layout = QtGui.QHBoxLayout(self)
alert_button = QtGui.QPushButton('Alert!', self, clicked=self.presenter.on_alert)
layout.addWidget(alert_button)
raise_button = QtGui.QPushButton('Raise exception!', self, clicked=self.presenter.on_raise_exception)
layout.addWidget(raise_button)
self.setLayout(layout)
def __del__(self):
super(InnerView, self).__del__()
self.logger.debug('Deleting InnerView (id:%s)' % id(self))
def show_alert(self):
QtGui.QMessageBox(text='Here is an alert').exec_()
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
app = QtGui.QApplication(sys.argv)
view = OuterView()
view.show()
sys.exit(app.exec_())
打开和关闭内部视图,您会看到视图和演示者都已按预期删除。打开内视图,点击按钮触发演示者异常,然后关闭内视图。视图将被删除,但演示者在应用程序退出之前不会被删除。
为什么?大概是代表 PySide 捕获所有异常的东西是存储对抛出它的对象的引用。为什么需要这样做?
我应该如何继续(当然,除了编写永远不会导致异常的代码)?我有足够的理由不依赖__del__ 进行资源管理。我知道我无权期望在捕获但未真正处理的异常之后发生任何事情,但这只是让我觉得不必要的丑陋。我一般应该如何处理这个问题?
【问题讨论】:
-
感谢您写出如此详尽的问题。 :)
-
@grayshirt 我使用 Python 2.7 在 Ubuntu 上运行,所以 Windows 和 Linux 在这里的行为似乎不同。
-
@grayshirt 另请注意:使用 PyQt4 而不是 PySide,我看到您在 Windows 上看到的相同行为。
-
我可以在我这边重现这个(我必须安装 PySide,因此延迟了)。我建议你使用
gc.get_referrers找出那些讨厌的引用是什么。 -
嗯,这是一个
<frame object at 0x7fc89c018ac8>持有参考。我认为异常拥有框架对象,因此可能是异常被缓存了。
标签: python exception-handling garbage-collection pyside