Search code examples
pythonooppyqt5garbage-collectionclass-attributes

What is the difference between the Garbage-Collection Mechanism in PyQT5 programs and normal conditions? (If this is a GC-related problem)


To learn about the Object-Deletion Features of metaclass QObject in PyQT5, I wrote the following code:

from PyQt5.Qt import *


class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setup_UI()
        self.setup_subUI()
        self.hook = None

    def setup_UI(self):
        self.setWindowTitle('MyWindowTitle')
        self.resize(600, 400)

    def setup_subUI(self):
        obj1 = QObject()
        obj2 = QObject(obj1)
        obj3 = QObject(obj2)
        self.hook = obj1  # This is line 19.
        obj1.setObjectName('obj1')
        obj2.setObjectName('obj2')
        obj3.setObjectName('obj3')
        obj1.destroyed.connect(lambda obj: print(f'Object {obj.objectName()} has been released'))
        obj2.destroyed.connect(lambda obj: print(f'Object {obj.objectName()} has been released'))
        obj3.destroyed.connect(lambda obj: print(f'Object {obj.objectName()} has been released'))


if __name__ == '__main__':
    import sys

    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

There are several obvious facts in the code above:

  1. The Window class has an non-essential attribute, hook, that was originally used to bind to obj1;
  2. obj1 is the parent object of obj2, obj2 is the parent object of obj3. If no objects are involved in Reference Counting, all three of them will be quickly recovered to activate the destroyed signal receiver below as well as print this message further.

To avoid GC, I added line 19 to try to keep them, self.hook = obj1, but it backfired.

Here's the puzzle:

If line 19 is changed to self.other_attribute_not_schedule = obj1, the program will not release these 3 objects before the Window UI entity is closed. More clearly, could you please tell me more details about the objects get deleted with the above code, and why they don't when I use the other line?


Solution

  • Garbage collection activates when the number of references of an object reaches 0. In that case, python tries to delete it, if Qt allows it.

    In your original code you first of all call setup_subUI, which creates a reference:

    self.hook = obj1
    

    Then, after setup_subUI returns, you do the following:

    self.hook = None
    

    This results in "overwriting" self.hook as a reference to None, so the previous reference (obj1) is removed. Since that was the only reference, it gets deleted, along with its children.

    Then you do the following instead, again in setup_subUI:

    self.other_attribute_not_schedule = obj1
    

    When setup_subUI returns, the next line will be evaluated as above:

    self.hook = None
    

    The difference here is that you're not overwriting self.hook since it didn't exist yet, and the obj1 is not deleted, because its reference (self.other_attribute_not_schedule) still exists.

    Note there are different types of references (for instance, adding an object to a persistent list) and they might not always be explicit, especially with complex modules like Qt.

    For instance, adding the parent to the constructor of the first object will prevent deletion, even if you do self.hook = None:

    obj1 = QObject(self)
    

    This is because the ownership of the object is then managed by Qt itself.
    There are many cases for which Qt takes ownership (by reparenting it if necessary) of an object, for instance when adding a widget to a layout and then setting the layout to another widget.