📜 ⬆️ ⬇️

PyQt. Manage memory, collect garbage

image
A long time ago, there was the C language. And there were 2 functions in it that control memory - malloc and free. But it was too difficult.
Bjarne Stroustrup looked at this and decided to make everything easier. 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 ++ is also not simple enough. He decided to go a different way and invented Python, in which even malloc and free are not.
In the meantime, the Norwegian trolls have created a Qt GUI library in C ++, which simplifies memory management for their objects by the fact that it deletes them when it sees fit.
Phil Thompson was upset that there is no great Qt library for a great Python language. And I decided to combine them with the PyQt project. However, as it turned out, if you cross different paradigms of memory management, side effects are sure to come out. Let's see what ...

* Historical justice and chronology are sacrificed to the artistic component of the entry.

The PyQt model of operation can be simplified as follows: for each public class C ++, a wrapper class is created in Python. The programmer works with the wrapper object, and it calls the methods of the “real” C ++ object.
All is well, as long as the object and the wrapper are synchronously created and synchronously die. But this synchronicity can be broken. I managed to do it in 3 ways:


Python wrapper is created, C ++ object is not



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 <module> >>> 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 MyObject constructor, we did not call the base class constructor. At the same time the object was successfully created, it can be used However, on the first attempt to call the C ++ method, we will get a RuntimeError with an explanation that we did not correctly.
Corrected version:

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


Python garbage collector deleted 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 longer any references to the label in the Python code, and the careful garbage collector removed the Python wrapper. In turn, the wrapper deleted 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 an object. Python wrapper alive


The previous 2 cases are described in the PyQt / Pyside documentation and are rather trivial. Much more complicated problems arise when the Python part doesn’t know that the Qt library has deleted the C ++ object.
Qt can delete an object when deleting a parent object, closing a window, calling deleteLater (), and in some other situations.
After deletion, 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 leg:
  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 <module> >>> 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 you try to call windowTitle () it 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, in real code, a more complex scenario often happens:
  1. Create an object
  2. We connect external signals to the object slots
  3. Qt deletes an object. For example, when closing a window
  4. The slot of the remote object is called by a timer or a signal from the outside world.
  5. The application crashes or generates an exception.


Long life 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 

A label is connected to the textChanged signal from QLineEdit. 1 second after launch, the label is closed and removed. The programmer and the user no longer need it. However, after 2 seconds, the remote label receives a signal. An exception is thrown at the console or the application unexpectedly crashes.


When slots do not turn off automatically


In C ++ applications, when an object is deleted, all its slots are disabled, so no problems arise. However, PyQt and PySide cannot always "disable" an object. It became interesting to me to understand when the slots are not disabled. During 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 methodslot is turned offslot is turned off
pure python method or functionthe fallslot is turned off
C ++ method - an object overloaded with Python-wrapperthe fallthe fall

Decision


With the removal of C ++ objects it is especially hard to fight. It sometimes appears not soon, and not at all clearly. Some tips:

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 you should be afraid of PyQt / PiSide? In practice, problems do not happen often. Any tool has strengths and weaknesses that you need to know.

Source: https://habr.com/ru/post/210304/


All Articles