Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
361 views
in Technique[技术] by (71.8m points)

pyqt - Is it safe to remove a Python reference to a Qt object directly after calling deleteLater()?

Please consider the minimal example below, which implements a custom QNetworkAccessManager that maintains a list of unfinished QNetworkReply instances.

When a reply is finished, it is removed from the unfinished_replies list.

As discussed in Is deleteLater() necessary in PyQt/PySide?, QNetworkReply.deleteLater() is used, inside the finished slot, to schedule the Qt object for deletion.

However, I am not sure what would be the best way to remove the Python reference to the reply object. I can think of two (mutually exclusive) options for removing the Python reference, as shown in the example below:

  1. remove directly after calling deleteLater()

  2. remove when the QNetworkReply.destroyed signal is emitted (docs)

Both options seem to work just fine. I would prefer option 1, but I'm not sure if this could lead to surprises in rare cases. Which would be best? Or is there another alternative?

import sys
from PyQt5 import QtNetwork, QtWidgets, QtCore


class CustomNetworkAccessManager(QtNetwork.QNetworkAccessManager):
    def __init__(self):
        super(CustomNetworkAccessManager, self).__init__()
        self.unfinished_replies = []
        self.finished.connect(self.slot)

    def get(self, *args, **kwargs):
        reply = super(CustomNetworkAccessManager, self).get(*args, **kwargs)
        reply.index = i  # just for printing
        self.unfinished_replies.append(reply)

    def remove_from_list(self, reply):
        self.unfinished_replies.remove(reply)
        print('{} unfinished replies left'.format(len(self.unfinished_replies)))
        if not self.unfinished_replies:
            QtCore.QCoreApplication.quit()

    def slot(self, reply):
        print('reply {} finished'.format(reply.index))
        # handle the Qt side:
        reply.deleteLater()  
        # handle the Python side:
        # either
        # OPTION 1 - remove now
        self.remove_from_list(reply)
        # or 
        # OPTION 2 - remove when destroyed
        # reply.destroyed.connect(lambda: self.remove_from_list(reply))


if __name__ == '__main__':
    # Initialize
    app = QtWidgets.QApplication(sys.argv)
    manager = CustomNetworkAccessManager()

    # Schedule requests
    url = 'http://httpbin.org/get'
    for i in range(6):
        manager.get(QtNetwork.QNetworkRequest(QtCore.QUrl(url)))

    # Start event loop
    app.exec_()

p.s. sorry for the Python 2 code

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Both are equivalent, they only differ the moment they are removed. But to understand more in detail you have to understand how the PyQt5/PySide2 binding works. Those libraries create a wrapper around the C++ object, something like:

class FooWrapper:
    def __new__(self, cls, *args, **kwargs):
         # ...
         instance = ...
         instance._cpp_object = create_cpp_object()
         # ...
         return instance

    def foo_method(self, *args, **kwargs):
        return execute_foo_method(self._cpp_object, *args, **kwargs)

    def __del__(self):
        destroyed_cpp_object(self._cpp_object)

So when calling deleteLater only the cpp_object is deleted and not the wrapper, you can verify that if you use:

reply.destroyed.connect(self.remove_from_list)
Traceback (most recent call last):
  File "main.py", line 32, in <lambda>
    reply.destroyed.connect(self.remove_from_list)
  File "main.py", line 17, in remove_from_list
    self.unfinished_replies.remove(reply)
ValueError: list.remove(x): x not in list

since the parameter passed by destroyed is an invalid wrapper getting the above error. For this case, a solution is to check if the cpp_object has been removed with sip.isdeleted():

from PyQt5 import QtNetwork, QtWidgets, QtCore
import sip

# ...
class CustomNetworkAccessManager(QtNetwork.QNetworkAccessManager):
    # ...

    def remove_from_list(self):
        self.unfinished_replies = [
            reply for reply in self.unfinished_replies if not sip.isdeleted(reply)
        ]
        print("{} unfinished replies left".format(len(self.unfinished_replies)))
        if not self.unfinished_replies:
            QtCore.QCoreApplication.quit()

    def slot(self, reply):
        print("reply {} finished".format(reply.index))
        # handle the Qt side:
        reply.deleteLater()
        # handle the Python side:
        reply.destroyed.connect(self.remove_from_list)

Returning to the study of your methods, these can be graphed as follows:

(FIRST METHOD)
------------┬------------------┬---------------------┬-----------------------
            |                  |                     |
 call_deleteLater remove_reply_from_list          destroyed
(SECOND METHOD)
------------┬-----------------------------------------┬-----------------┬------
            |                                         |                 |
 call_deleteLater                               destroyed remove_reply_from_list

And since you are using the original wrappers you should not have any problem.

Conclusion: Both are equivalent and safe.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...