Search code examples
pythonpyqtpyqt5destroyqgraphicsitem

When does QGraphicsItem get destroyed in PyQt?


It seems that a QGraphicsItem status goes from To be destroyed by: C/C++ to To be destroyed by: Python, while not destroyed and still accessible. Is it an expected behavior ? If so, can someone explain ?

Creation part

node = QGraphicsRectItem()
self.scene.addItem(node)

print("Deleted ? ", sip.isdeleted(node))
print("Owned by Python ? ", sip.ispyowned(node))
sip.dump(node)

Output

Deleted ?  False
Owned by Python ?  False
<PyQt5.QtWidgets.QGraphicsRectItem object at 0x7fcdb82371f8>
    Reference count: 3
    Address of wrapped object: 0x214bf80
    Created by: Python
    To be destroyed by: C/C++
    Parent wrapper: <PyQt5.QtWidgets.QGraphicsScene object at 0x7fcdb821ea68>
    Next sibling wrapper: <__main__.Host object at 0x7fcdb8237dc8>
    Previous sibling wrapper: NULL
    First child wrapper: NULL

Deletion part

self.scene.removeItem(node)

print("Deleted ? ", sip.isdeleted(node))
print("Owned by Python ? ", sip.ispyowned(node))
sip.dump(node)

Output

Deleted ?  False
Owned by Python ?  True
<PyQt5.QtWidgets.QGraphicsRectItem object at 0x7fcdb82371f8>
    Reference count: 2
    Address of wrapped object: 0x214bf80
    Created by: Python
    To be destroyed by: Python
    Parent wrapper: NULL
    Next sibling wrapper: NULL
    Previous sibling wrapper: NULL
    First child wrapper: NULL

One can see it is now owned by Python after deletion. And it still exists. Why ?


Solution

  • When you add the item to the scene, Qt takes ownership of it, so you don't need to keep an explicit reference to it on the Python side. This means that when the scene is deleted, the C++ part of the item will be deleted as well - and if there are no other references held to the Python part, it will also be garbage-collected (so there will be nothing left).

    When you remove the item from the scene, ownership passes back to the caller, who now has full responsiblity for deleting it. If QGraphicsRectItem inherited QObject, you could call deleteLater() to delete the C++ part - but this would still leave you with the empty Python wrapper, so it wouldn't really make much difference.

    The way to delete a PyQt wrapper is the same as for any other python object - i.e. use del, or simply let it go out of scope and allow it to be garbage-collected in the normal way. There is no special PyQt way to delete such objects, because nothing special is needed.

    In your example, you never actually make any attempt to delete the item, so the information reported by sip is entirely as expected.

    Finally, note that it is always possible to delete the C++ part independently of the PyQt wrapper. If you do this, and then attempt to access the item's methods, you will get a RuntimeError:

    >>> scene = QGraphicsScene()
    >>> node = QGraphicsRectItem()
    >>> scene.addItem(node)
    >>> del scene
    >>> node.rect()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    RuntimeError: wrapped C/C++ object of type QGraphicsRectItem has been deleted
    

    UPDATE:

    Below is a demo that shows how the item is garbage-collected when it is removed from the scene and allowed to go out of scope. Of course, if there are any references held to the item elsewhere (i.e. if ref-count > 1), the item will not be deleted.

    import sip, gc
    from PyQt5 import QtCore, QtWidgets
    
    app = QtWidgets.QApplication(['test'])
    
    scene = QtWidgets.QGraphicsScene()
    
    def test():
        node = QtWidgets.QGraphicsRectItem()
        scene.addItem(node)
        scene.removeItem(node)
        print("Deleted ? ", sip.isdeleted(node))
        print("Owned by Python ? ", sip.ispyowned(node))
        sip.dump(node)
        print()
    
    test()
    
    print('gc collect...')
    
    gc.collect()
    
    for obj in gc.get_objects():
        if isinstance(obj, QtWidgets.QGraphicsRectItem):
            print(obj)
            break
    else:
        print('deleted')
    

    Output:

    Deleted ?  False
    Owned by Python ?  True
    <PyQt5.QtWidgets.QGraphicsRectItem object at 0x7f90f66d5798>
        Reference count: 3
        Address of wrapped object: 0x205bd80
        Created by: Python
        To be destroyed by: Python
        Parent wrapper: NULL
        Next sibling wrapper: NULL
        Previous sibling wrapper: NULL
        First child wrapper: NULL
    
    gc collect...
    deleted