【问题标题】:Why is PySide's exception handling extending this object's lifetime?为什么 PySide 的异常处理会延长这个对象的生命周期?
【发布时间】: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


【解决方案1】:

问题是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,所以不会造成大量伤害。

但如果你想回收内存,只需删除这些值。

【讨论】:

  • 启发——我错误地认为这是 PySide 的错;它只是 Python。仍然对我的应用程序构成问题。长话短说,在抛出异常后,Presenter 可以在其 View 被关闭和删除后继续存在。在没有自然死亡的情况下,它会继续收到pypubsub 消息,要求它更新其不再存在的视图。简单的解决方案:在更新之前测试视图是否存在。避免进一步的例外,但对我来说,Presenter 的寿命比它的 View 长。我看不到任何删除您上面描述的值的好地方。有什么建议吗?
  • 我不太确定。通常没有引用它们的东西不应该做任何事情,但我知道在你的情况下这要困难得多。但是您不应该要求任何销毁命令; Python 不保证__del__永远被调用,对象也不会永远被删除。
  • 最重要的是,正如我们在上面的 cmets 中注意到的那样,无论出于何种原因,在 Ubuntu 上,PySide 都不会破坏 View 对象(以及它们的 Presenter),无论是否异常。似乎选择是与此和平相处或去学习 C++。再次感谢您的帮助。
猜你喜欢
  • 2019-01-06
  • 1970-01-01
  • 2022-01-11
  • 1970-01-01
  • 1970-01-01
  • 2015-04-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多