2

I would like to understand how Python's and PyQt's garbage collectors work. In the following example, I create a QWidget (named TestWidget) that has a python attribute 'x'. I create TestWidget, interact with it, then close its window. Since I have set WA_DeleteOnClose, this should signal the Qt event loop to destroy my instance of TestWidget. Contrary to what I expect, at this point (and even after the event loop has finished) the python object referenced by TestWidget().x still exists.

I am creating an app with PyQt where the user opens and closes many many widgets. Each widget has attributes that take up a substantial amount of memory. Thus, I would like to garbage collect (in both Qt and Python) this widget and its attributes when the user closes it. I have tried overriding closeEvent and deleteEvent to no success.

Can someone please point me in the right direction? Thank you! Example code below:

from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit
from PyQt5.QtCore import Qt


class TestWidget(QWidget):

    def __init__(self, parent, **kwargs):
        super().__init__(parent, **kwargs)
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.widget = None
        self.x = '1' * int(1e9)

    def load(self):
        layout = QVBoxLayout(self)
        self.widget = QTextEdit(parent=self)
        layout.addWidget(self.widget)
        self.setLayout(layout)


if __name__ == '__main__':
    from PyQt5.QtWidgets import QApplication
    import gc

    app = QApplication([])
    widgets = []
    widgets.append(TestWidget(parent=None))
    widgets[-1].load()
    widgets[-1].show()
    widgets[-1].activateWindow()

    app.exec()

    print(gc.get_referrers(gc.get_referrers(widgets[-1].x)[0]))
CC BY-SA 4.0
1

2 Answers 2

Reset to default
2

An important thing to remember is that PyQt is a binding, any python object that refers to an object created on Qt (the "C++ side") is just a wrapper.

The WA_DeleteOnClose only destroys the actual QWidget, not its python object (the TestWidget instance).

In your case, what's happening is that Qt releases the widget, but you still have a reference on the python side (the element in your list): when the last line is executed, the widgets list and its contents still exist in that scope.

In fact, you can try to add the following at the end:

    print(widgets[-1].objectName())

And you'll get the following exception:

Exception "unhandled RuntimeError"
wrapped C/C++ object of type TestWidget has been deleted

When also the python object is deleted, then all its attributes are obviously deleted as well.

To clarify, see the following:

class Attribute(object):
    def __del__(self):
        print('Deleting Attribute...')

class TestWidget(QWidget):

    def __init__(self, parent, **kwargs):
        super().__init__(parent, **kwargs)
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.widget = None
        self.x = Attribute()

    def load(self):
        layout = QVBoxLayout(self)
        self.widget = QTextEdit(parent=self)
        layout.addWidget(self.widget)
        self.setLayout(layout)

    def __del__(self):
        print('Deleting TestWidget...')

You'll see that __del__ doesn't get called in any case with your code.
Actual deletion will happen if you add del widgets[-1] instead.

CC BY-SA 4.0
1
  • 1
    Strictly speaking, the deletion of the qt-widget and/or it's pyqt wrapper sometimes won't be enough. The real issue is what remaining references are there to the specific memory-hungry objects themselves? The widget's attributes might represent the only ones left, but that's not guaranteed. In a real-world application, there are many more places where unexpected references can hide.Commented Mar 17, 2021 at 12:25
1

Explanation

To understand the problem, you must know the following concepts:

  • Python objects are eliminated only when they no longer have references, for example in your example when adding the widget to the list then a reference is created, another example is that if you make an attribute of the class then a new reference is created.

  • PyQt (and also PySide) are wrappers of the Qt library (See 1 for more information), that is, when you access an object of the QFoo class from python, you do not access the C++ object but the handle of that object. For this reason, all the memory logic that Qt creates is handled by Qt but those that the developer creates has to be handled by himself.

Considering the above, what the WA_DeleteOnClose flag does is eliminate the memory of the C++ object but not the python object.

To understand how memory is being handled, you can use the memory-profiler tool with the following code:

from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit
from PyQt5.QtCore import Qt, QTimer
from PyQt5 import sip

class TestWidget(QWidget):
    def __init__(self, parent, **kwargs):
        super().__init__(parent, **kwargs)
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.widget = None
        self.x = ""

        period = 1000

        QTimer.singleShot(4 * period, self.add_memory)
        QTimer.singleShot(8 * period, self.close)
        QTimer.singleShot(12 * period, QApplication.quit)

    def load(self):
        layout = QVBoxLayout(self)
        self.widget = QTextEdit()
        layout.addWidget(self.widget)

    def add_memory(self):
        self.x = "1" * int(1e9)


if __name__ == "__main__":
    from PyQt5.QtWidgets import QApplication
    import gc

    app = QApplication([])
    app.setQuitOnLastWindowClosed(False)

    widgets = []

    widgets.append(TestWidget(parent=None))
    widgets[-1].load()
    widgets[-1].show()
    widgets[-1].activateWindow()

    app.exec()

enter image description here

In the previous code, in second 4 the memory of "x" is created, in second 8 the C++ object is eliminated but the memory associated with "x" is not eliminated and this is only eliminated when the program is closed since it is clear the list and hence the python object reference.

Solution

In this case a possible solution is to use the destroyed signal that is emitted when the C++ object is deleted to remove all references to the python object:

from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit
from PyQt5.QtCore import Qt, QTimer
from PyQt5 import sip


class TestWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.widget = None
        self.x = ""

        period = 1000

        QTimer.singleShot(4 * period, self.add_memory)
        QTimer.singleShot(8 * period, self.close)
        QTimer.singleShot(12 * period, QApplication.quit)

    def load(self):
        layout = QVBoxLayout(self)
        self.widget = QTextEdit()
        layout.addWidget(self.widget)

    def add_memory(self):
        self.x = "1" * int(1e9)


class Manager:
    def __init__(self):
        self._widgets = []

    @property
    def widgets(self):
        return self._widgets

    def add_widget(self, widget):
        self._widgets.append(widget)
        widget.destroyed.connect(self.handle_destroyed)

    def handle_destroyed(self):
        self._widgets = [widget for widget in self.widgets if not sip.isdeleted(widget)]


if __name__ == "__main__":
    from PyQt5.QtWidgets import QApplication

    app = QApplication([])
    app.setQuitOnLastWindowClosed(False)

    manager = Manager()
    manager.add_widget(TestWidget())
    manager.widgets[-1].load()
    manager.widgets[-1].show()
    manager.widgets[-1].activateWindow()

    app.exec()

enter image description here

CC BY-SA 4.0

    Your Answer

    By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

    Not the answer you're looking for? Browse other questions taggedor ask your own question.