pythonuser-interfacepyqt5signals-slots

Do I need to manually disconnect signals if I am going to call deleteLater on a PyQt widget?


I have a QMainWidnow that spawns a number of different dialog boxes, I have been manually disconnecting all the signals before deleting the dialogs. Is this necessary, or are they cleaned up by the garbage collector or the PyQt backend?

class AlignmentStationInterface(QMainWindow):
    def __init__(self)
        super().__init__()
        add_button = QPushButton("Add Position")
        add_button.clicked.connect(self.add_position)
        self.position_builder_dialog = None

    def add_position(self):
        print('write new location')
        if self.new_position_builder_popup is not None:
            return
        self.position_builder_dialog = PositionBuilderDialog()
        self.position_builder_dialog.new_position_return.connect(self.save_position)
        self.position_builder_dialog.finished.connect(self.cleanup_dialog)

    def cleanup_dialog(self):
        # Are these disconnect lines necessary?
        self.position_builder_dialog.new_position_return.disconnect(self.save_position)
        self.position_builder_dialog.finished.disconnect(self.cleanup_dialog)
        self.position_builder_dialog.deleteLater()

    def save_position(self):
        pass
        

I expect that the backend takes care of things but I am worried about memory leaks.


Solution

  • In theory, it's not necessary, but it depends.

    Signals are normally disconnected automatically whenever one of their parts are destroyed: as long as the signals are connected to a function of a destroyed QObject, they are usually automatically disconnected upon deletion.

    But you need to remember that using a Python binding such as PyQt (and PySide) you always end up with two objects: the "Python object" and the "Qt object" (the one existing in the "C++ world").

    This means that Python objects and Qt objects can have different lifespans:

    Your code is exemplary: you are creating a python reference to a Qt wrapped object, but deleteLater() will only destroy the C++ counterpart, because self.position_builder_dialog.deleteLater() will not delete the self.position_builder_dialog object in the Python interpreter.

    In fact, if the button is clicked again after cleanup_dialog() is called (calling add_position() in turn), nothing will happen because self.new_position_builder_popup is not None. While the C++ object has been destroyed, the Python reference still exists.

    In theory, even after the dialog has been actually destroyed (with deleteLater()), you could still call methods of PositionBuilderDialog as long as they don't rely on Qt functions strictly related to that dialog. That's because those methods exist in the Python namespace, and they still "make sense".

    Let's suppose the following basic example:

    class PositionBuilderDialog(QDialog):
        def __init__(self, parent=None):
            super().__init__(parent)
    
            self.value = ''
            self.lineEdit = QLineEdit()
    
            layout = QVBoxLayout(self)
            layout.addWidget(self.lineEdit)
    
            self.lineEdit.textChanged.connect(self.edited)
    
        def edited(self, text):
            self.value = text
    

    Then consider the following situation:

    class AlignmentStationInterface(QMainWindow):
        ...
        def add_position(self):
            print('write new location')
            if self.new_position_builder_popup is not None:
                print(self.new_position_builder_popup.value)
                print(self.new_position_builder_popup.lineEdit)
                print(self.new_position_builder_popup.lineEdit.text())
                return
            ...
    

    After the button has been clicked once and the dialog has been closed (then deleted in cleanup_dialog()), clicking the button will do the following:

    1. enter the if block, because new_position_builder_popup is not None;
    2. print the content of the dialog's value;
    3. print the Python reference to the lineEdit object;
    4. crash because text() would attempt to call the related function of the wrapped C++ object, which doesn't exist anymore;

    In your case, you don't need to disconnect the signals, but you do need to actually remove the Python reference:

        def cleanup_dialog(self):
            self.position_builder_dialog.deleteLater()
            self.position_builder_dialog = None
    

    In principle, calling deleteLater() is normally required, but also ensuring that all Python references are also deleted is usually recommended. The former will actually delete the real Qt object (which will free up much of the memory the program is being using for it), while the latter is usually required mostly for reliable code: before worrying about memory leaks, you should worry about usage of object references.

    Note that signal connections can create further closures in some cases (especially when using lambdas), meaning that the Python objects could still exist even if they have been theoretically destroyed. Remember that you can never completely rely on garbage collection when dealing with wrapped objects in bindings, and that lifespans of objects may be completely inconsistent.

    Take the following code, based on the above:

        def add_position(self):
            dialog = PositionBuilderDialog(self) # note the "self" argument
            dialog.show()
    

    In the Python world, that would automatically garbage collect dialog. Still, the dialog will be shown without problems. That's because the dialog has been created with a parent, which ensures that it will always be kept alive as long as the parent it. Qt doesn't know anything about Python, it only knows that the dialog has a parent, and then it will not destroy it, until that parent is.