Back to Home

PyQt. Manage memory, collect garbage

python · pyqt · pyqt4 · pyside

PyQt. Manage memory, collect garbage

    image
    The C language was long ago. And there were 2 functions controlling memory in it - malloc and free. But it was too complicated.
    Björn Straustrup looked at it and decided that it was necessary to make everything simpler. And invented C ++. In addition to malloc / free, new / delete, destructors, RAII, auto and shared pointers appeared there.
    Guido van Rossum looked at this, and decided that C ++ was also not simple enough. He decided to go the other way and came up with Python, in which even malloc and free are not.
    Meanwhile, the Norwegian trolls created in C ++ the Qt GUI library, which simplifies memory management for their objects due to the fact that they themselves delete them when they see fit.
    Phil Thompson was upset that there wasn’t a great Qt library for the wonderful Python language. And I decided to combine them with the PyQt project. However, as it turned out, if you cross the different paradigms of memory management, side effects will certainly come out. Let's see which ...

    * Historical justice and chronology are sacrificed for the artistic component of the introduction.

    The PyQt work model can be simplified as follows: for each public C ++ class, a wrapper class is created in Python. The programmer works with the wrapper object, and it calls the methods of the "real" C ++ object.
    Everything is fine as long as the object and wrapper are synchronously created and synchronously die. But this synchronism can be broken. I managed to do this in 3 ways:
    • Python wrapper created, C ++ object no
    • Python garbage collector deleted the desired object
    • Qt deleted the object. Python wrapper alive


    Python wrapper created, C ++ object no



        from PyQt4.QtCore import QObject
        class MyObject(QObject):
            def __init__(self):
                self.field = 7
        obj = MyObject()
        print(obj.field)
        obj.setObjectName("New object")
    >>> Traceback (most recent call last):
    >>>   File "pyinit.py", line 9, in 
    >>>     obj.setObjectName("New object")
    >>> RuntimeError: '__init__' method of object's base class (MyObject) not called.
    


    This and other examples can be found here.

    In the constructor of MyObject, we did not call the constructor of the base class. In this case, the object was successfully created, it can be used. However, the first time we try to call the C ++ method, we get a RuntimeError with an explanation of what we did wrong.
    Corrected version:

        ...
        class MyObject(QObject):
            def __init__(self):
                QObject.__init__(self)
        ...
    


    Python garbage collector deleted the desired object


        from PyQt4.QtGui import QApplication, QLabel
        def createLabel():
            label = QLabel("Hello, world!")
            label.show()
        app = QApplication([])
        createLabel()
        app.exec_()
    


    If this code were written in C ++, after app.exec_ () we would get a window with “Hello, world!”. But, this code will not show anything. When the createLabel () function finished executing, there were no more label references in the Python code , and the caring garbage collector removed the Python wrapper. In turn, the wrapper removed the C ++ object.

    Corrected version:
        from PyQt4.QtGui import QApplication, QLabel
        def createLabel():
            label = QLabel("Hello, world!")
            label.show()
            return label
        app = QApplication([])
        label = createLabel()
        app.exec_()
    

    We keep links to all created objects, even if we are not going to use these links.

    Qt deleted the object. Python wrapper alive


    The previous 2 cases are described in the PyQt / Pyside documentation and are pretty trivial. Much more complex problems arise when the Python part is unaware that the Qt library has deleted the C ++ object.
    Qt can delete an object by deleting the parent, closing the window, calling deleteLater (), and in some other situations.
    After removal, you can work with wrapper methods written in pure Python, and an attempt to access the C ++ part causes a RuntimeError or application crash .

    Let's start with a very simple way to shoot yourself in the foot:
        from PyQt4.QtCore import QTimer
        from PyQt4.QtGui import QApplication, QWidget
        app = QApplication([])
        widget = QWidget()
        widget.setWindowTitle("Dead widget")
        widget.deleteLater()
        QTimer.singleShot(0, app.quit)  # Делаем так, чтобы приложение завершилось сразу после старта
        app.exec_()  #  Запускаем приложение, чтобы оно выполнило deleteLater()
        print(widget.windowTitle())
    >>> Traceback (most recent call last):
    >>>   File "1_basic.py", line 20, in 
    >>>     print(widget.windowTitle())
    >>> RuntimeError: wrapped C/C++ object of type QWidget has been deleted
    


    Create a QWidget, ask Qt to remove it. During app.exec_ (), the object will be deleted. The wrapper does not know about this, and when trying to call windowTitle () will throw an exception or cause a crash.
    Of course, if the programmer called deleteLater () and then uses the object, then he himself is to blame. However, a more complex scenario often happens in real code:
    1. Create an object
    2. We connect external signals to the object slots
    3. Qt removes the object. For example, when closing a window
    4. The slot of a remote object is called by a timer or a signal from the outside world.
    5. Application crashes or throws an exception


    A long life-long example
        from PyQt4.QtCore import Qt, QTimer
        from PyQt4.QtGui import QApplication, QLabel, QLineEdit
        def onLineEditTextChanged():
            print('~~~~ Line edit text changed')
        def onLabelDestroyed():
            print('~~~~ C++ label object destroyed')
        def changeLineEditText():
            print('~~~~ Changing line edit text')
            lineEdit.setText("New text")
        class Label(QLabel):
            def __init__(self):
                QLabel.__init__(self)
                self.setAttribute(Qt.WA_DeleteOnClose)
                self.destroyed.connect(onLabelDestroyed)
            def __del__(self):
                print('~~~~ Python label objВ качестве источника внешних сигналов используется QLineEdit, а в качестве удаляемого объекта - Label.ect destroyed')
            def setText(self, text):
                print('~~~~ Changing label text')
                QLabel.setText(self, text)
            def close(self):
                print('~~~~ Closing label')
                QLabel.close(self)
        app = QApplication([])
        app.setQuitOnLastWindowClosed(False)
        label = Label()
        label.show()
        lineEdit = QLineEdit()
        lineEdit.textChanged.connect(onLineEditTextChanged)
        lineEdit.textChanged.connect(label.setText)
        QTimer.singleShot(1000, label.close)   # пользователь закрыл одно из окон
        QTimer.singleShot(2000, changeLineEditText)  # пользователь изменил текст в другом окне. Произошло исключение.
        QTimer.singleShot(3000, app.quit)
        app.exec_()
        print('~~~~ Application exited')
    >>> ~~~~ Closing label
    >>> ~~~~ C++ label object destroyed
    >>> ~~~~ Changing line edit text
    >>> ~~~~ Line edit text changed
    >>> ~~~~ Changing label text
    >>> Traceback (most recent call last):
    >>>   File "2_reallife.py", line 33, in setText
    >>>     QLabel.setText(self, text)
    >>> RuntimeError: wrapped C/C++ object of type Label has been deleted
    >>> ~~~~ Application exited
    >>> ~~~~ Python label object destroyed
    

    Label is connected to the textChanged signal from QLineEdit. 1 second after starting, the label closes and is deleted. The programmer and user no longer need it. However, after 2 seconds, the remote label receives a signal. An exception is thrown into the console or the application crashes unexpectedly.


    When the slots do not turn off automatically


    In C ++ applications, when an object is deleted, all its slots are disabled, so there are no problems. However, PyQt and PySide cannot always “disconnect” an object. It became interesting to me to understand when the slots did not turn off. In the course of the experiments, the following test was born:

    More code
        PYSIDE = False
        USE_SINGLESHOT = True
        if PYSIDE:
            from PySide.QtCore import Qt, QTimer
            from PySide.QtGui import QApplication, QLineEdit
        else:
            from PyQt4.QtCore import Qt, QTimer
            from PyQt4.QtGui import QApplication, QLineEdit
        def onLineEditDestroyed():
            print('~~~~ C++ lineEdit object destroyed')
        def onSelectionChanged():
            print('~~~~ Pure C++ method selectAll() called')
        class LineEdit(QLineEdit):
            def __init__(self):
                QLineEdit.__init__(self)
                self.setText("foo bar")
                self.destroyed.connect(onLineEditDestroyed)
                #self.selectionChanged.connect(onSelectionChanged)
            def __del__(self):
                print('~~~~ Python lineEdit object destroyed')
            def clear(self):
                """Overridden Qt method
                """
                print('~~~~ Overridden method clear() called')
                QLineEdit.clear(self)
            def purePythonMethod(self):
                """Pure python method.
                Does not override any C++ methods
                """
                print('~~~~ Pure Python method called')
                self.windowTitle()  # generate exception
        app = QApplication([])
        app.setQuitOnLastWindowClosed(False)
        lineEdit = LineEdit()
        lineEdit.deleteLater()
        if USE_SINGLESHOT:
            #QTimer.singleShot(1000, lineEdit.clear)
            #QTimer.singleShot(1000, lineEdit.purePythonMethod)
            QTimer.singleShot(1000, lineEdit.selectAll)  # pure C++ method
        else:
            timer = QTimer(None)
            timer.setSingleShot(True)
            timer.setInterval(1000)
            timer.start()
            #timer.timeout.connect(lineEdit.clear)
            #timer.timeout.connect(lineEdit.purePythonMethod)
            timer.timeout.connect(lineEdit.selectAll)  # pure C++ method
        QTimer.singleShot(2000, app.quit)
        app.exec_()
        print('~~~~ Application exited')
    



    As it turned out, the result depends on which slots of the remote object were connected to the signals. The behavior is slightly different in PyQt and PySide.
    Slot typePyqtPyside
    C ++ object methodthe slot is disconnectedthe slot is disconnected
    method or function in pure Pythona fallthe slot is disconnected
    C ++ method - object overloaded with Python wrappera falla fall

    Decision


    Deleting C ++ objects is especially hard to deal with. It sometimes appears not soon, and not at all explicitly. Some tips:
    • If you intend to delete an object that has Python slots, manually disconnect the object from external signals
    • To track the moment the object was deleted, you can use the QObject.destroyed signal , but not the Python wrapper __del__ method
    • Do not use QTimer.singleShot for objects that can be deleted. Such a timer cannot be stopped.

    If there is a silver bullet, I will be glad to read about it in the comments.

    Conclusion


    I hope no one has concluded that PyQt / PiSide should be scared? In practice, problems do not happen often. Any tool has strengths and weaknesses that you need to know.

    Read Next