tl;dr -- In a PySide application, an object whose method throws an exception will remain alive even when all other references have been deleted. Why? And what, if anything, should one do about this?
In the course of building a simple CRUDish app using a Model-View-Presenter architecture with a PySide GUI, I discovered some curious behavior. In my case:
weakref.ref
) to avoid circularitypypubsub
messaging library, but this also stores only weak references to listeners, and is not a factor in the MCVE below.)However, a Presenter of which a method has thrown an exception does not get deleted as expected. The application continues to function, because PySide employs some magic to catch exceptions. The Presenter in question continues to receive and respond to any View events bound to it. But when the View is deleted, the exception-throwing Presenter remains alive until the whole application is closed. An 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_())
Open and close the inner view, and you'll see both view and presenter are deleted as expected. Open the inner view, click the button to trigger an exception on the presenter, then close the inner view. The view will be deleted, but the presenter won't until the application exits.
Why? Presumably whatever it is that catches all exceptions on behalf of PySide is storing a reference to the object that threw it. Why would it need to do that?
How should I proceed (aside from writing code that never causes exceptions, of course)? I have enough sense not to rely on __del__
for resource management. I get that I have no right to expect anything subsequent to a caught-but-not-really-handled exception to go ideally but this just strikes me as unnecessarily ugly. How should I approach this in general?
The problem is sys.last_tracback
and sys.last_value
.
When a traceback is raised interactively, and this seems to be what is emulated, the last exception and its traceback are stores in sys.last_value
and sys.last_traceback
respectively.
Doing
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
will free the memory.
It's worth noting that at most one exception and traceback pair can get cached. This means that, because you're sane and don't rely on del
, there isn't a massive amount of damage to be done.
But if you want to reclaim the memory, just delete those values.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With