Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PyQt4 setParent vs deleteLater

I have a layout that I add a lot of custom widgets to with something like layout.addWidget(widget). Later I want to remove all those custom widgets and add new ones. I'm confused on the best way to do this when it comes to deleteLater and setParent(None). For example, here's my cleanup function that's called for all the widgets in the layout:

def _removeFilterWidgetFromLayout(self, widget):
    """Remove filter widget"""

    self._collection_layout.removeWidget(widget)
    widget.setParent(None)
    widget.deleteLater()

I have a suspicion that not all of these lines are needed to properly cleanup the memory used by the widget. I'm unsure of what is happening with the C++ and Python references to widget.

Here's what I understand about the C++ and Python references to QObjects in PyQt.

I believe that when you add a widget to a layout the widget becomes a child of the layout. So, if I call removeWidget then the parent relationship is broken so I have to clean up the C++ and Python reference myself since the widget has no other parent. The call to setParent is an explicit way of removing the parent relationship with the layout. The call to deleteLater is meant to take care of the C++ reference.

The Python reference is garbage collected because the widget variable goes out of scope, and there are no other Python objects pointing to widget.

Do I need to call setParent and deleteLater or would deleteLater be enough to properly clean this up?

As a side note, I've found that calling setParent(None) is a very expensive function call in this scenario. I can greatly speed up my entire cleanup process by removing this call. I'm not sure if deleteLater is enough to clean everything up correctly. Here's my profiling output from line_profiler:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
  2167                                               @profile
  2168                                               def _removeFilterWidgetFromLayout(self, widget):
  2169                                                   """Remove filter widget"""
  2170
  2171       233         1528      6.6      1.0          self._collection_layout.removeWidget(widget)
  2172       233       143998    618.0     97.9          widget.setParent(None)
  2173       233         1307      5.6      0.9          widget.deleteLater()

When using PyQt4 is there an 'accepted' way to do this cleanup? Should I use setParent, deleteLater, or both?

like image 395
durden2.0 Avatar asked May 14 '15 15:05

durden2.0


1 Answers

Probably the easiest way to see what's actually going on is to step through things in an interactive session:

>>> parent = QtGui.QWidget()
>>> child = QtGui.QWidget()
>>> layout = QtGui.QHBoxLayout(parent)
>>> layout.addWidget(child)
>>> child.parent() is layout
False
>>> child.parent() is parent
True

So the layout does not become the parent of the widget. This makes sense, because widgets can only have other widgets as parents, and layouts are not widgets. All widgets added to a layout will eventually have their parents reset to the parent of the layout (whenever it gets one).

>>> item = layout.itemAt(0)
>>> item
<PyQt4.QtGui.QWidgetItem object at 0x7fa1715fe318>
>>> item.widget() is child
True

Since there is no parent/child relationship bewteen layouts and the widgets they contain, a different API is needed for access to the underlying objects. The items are owned by the layout, but the ownership of the underlying objects remains unchanged.

>>> layout.removeWidget(child)
>>> child.parent() is parent
True
>>> layout.count()
0
>>> repr(layout.itemAt(0))
'None'
>>> item
<PyQt4.QtGui.QWidgetItem object at 0x7fa1715fe318>

At this point, the layout has deleted its item (because it had ownership of it), and thus no longer holds any references to the contained widget. Given this, it is no longer safe to do much with the python wrapper for the item (the interpreter would probably crash if we tried to call any of its methods).

>>> child.deleteLater()
>>> parent.children()
[<PyQt4.QtGui.QHBoxLayout object at 0x7fa1715fe1f8>]
>>> child.parent()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: wrapped C/C++ object of type QWidget has been deleted
>>>

Since we still have ownership of the child widget, we can call deleteLater on it. And as can be seen from the traceback, this will delete the underlying C++ object, but its python wrapper object will be left behind. However, this wrapper will (eventually) be deleted by the garbage collector, once any remaining python references to it are gone. Note that there is never any need to call setParent(None) during this process.

One final point: the above interpreter session is slightly misleading, because the event-queue is processed every time a line is executed. This means the effects of deleteLater are seen immediately, which would not be the case if the code was run as a script. To get immediate deletion in a script, you would need to use the sip module:

>>> import sip
>>> sip.delete(child)
like image 178
ekhumoro Avatar answered Nov 05 '22 05:11

ekhumoro