为什么PySide的异常处理延长了这个对象的生命周期?

时间:2014-09-28 15:18:45

标签: python exception-handling garbage-collection pyside

tl; dr - 在PySide应用程序中,即使已删除所有其他引用,其方法抛出异常的对象仍将保持活动状态。为什么?什么,如果有的话,应该怎么做呢?

在使用带有PySide GUI的Model-View-Presenter架构构建简单CRUDish应用程序的过程中,我发现了一些奇怪的行为。就我而言:

  • 界面分为多个视图 - 即显示不同数据方面的每个标签页可能是其自己的视图类
  • 首先实例化视图,并在初始化时,实例化自己的Presenter,保持对它的正常引用
  • Presenter接收对View it drive的引用,但将其存储为弱引用(weakref.ref)以避免循环
  • 不存在对Presenter的其他强引用。 (演示者可以间接与pypubsub消息库进行通信,但这也仅存储对侦听器的弱引用,并且不是下面MCVE中的一个因素。)
  • 因此,在正常操作中,当删除视图时(例如,当关闭选项卡时),随着其引用计数变为0,其随后的Presenter将被删除

但是,方法抛出异常的Presenter不会按预期删除。应用程序继续运行,因为PySide使用some magic来捕获异常。有问题的演示者继续接收并响应与其绑定的任何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__进行资源管理。我知道我没有权利期待一个被抓住但没有真正处理过的异常之后的任何事情理想地发生,但这只会让我感到不必要的丑陋。我应该如何处理这个问题?

1 个答案:

答案 0 :(得分:3)

问题是sys.last_tracbacksys.last_value

当以交互方式引发回溯时,这似乎是模拟的内容,最后一个异常及其回溯分别是sys.last_valuesys.last_traceback中的存储。

否则

del sys.last_value
del sys.last_traceback

# for consistency, see
# https://docs.python.org/3/library/sys.html#sys.last_type
del sys.last_type

将释放记忆。

值得注意的是,最多一个异常和回溯对可以缓存。这意味着,因为你的理智并且不依赖于del,所以不会造成大量的伤害。

但是如果要回收内存,只需删除这些值即可。