Skip to main content

Ralsina.Me — Roberto Alsina's website

PyQt Quickie: Don't Get Garbage Collected

There is one area where Qt and Python (and in con­se­quence PyQt) have ma­jor dis­agree­ments. That area is mem­o­ry man­age­men­t.

While Qt has its own mech­a­nisms to han­dle ob­ject al­lo­ca­tion and dis­pos­al (the hi­er­ar­chi­cal QOb­ject trees, smart point­er­s, etc.), PyQt runs on Python, so it has garbage col­lec­tion.

Let's con­sid­er a sim­ple ex­am­ple:

from PyQt4 import QtCore

def finished():
    print "The process is done!"
    # Quit the app
    QtCore.QCoreApplication.instance().quit()

def launch_process():
    # Do something asynchronously
    proc = QtCore.QProcess()
    proc.start("/bin/sleep 3")
    # After it finishes, call finished
    proc.finished.connect(finished)

def main():
    app = QtCore.QCoreApplication([])
    # Launch the process
    launch_process()
    app.exec_()

main()

If you run this, this is what will hap­pen:

QProcess: Destroyed while process is still running.
The process is done!

Plus, the script never ends. Fun! The problem is that proc is being deleted at the end of launch_process because there are no more references to it.

Here is a bet­ter way to do it:

from PyQt4 import QtCore

processes = set([])

def finished():
    print "The process is done!"
    # Quit the app
    QtCore.QCoreApplication.instance().quit()

def launch_process():
    # Do something asynchronously
    proc = QtCore.QProcess()
    processes.add(proc)
    proc.start("/bin/sleep 3")
    # After it finishes, call finished
    proc.finished.connect(finished)

def main():
    app = QtCore.QCoreApplication([])
    # Launch the process
    launch_process()
    app.exec_()

main()

Here, we add a global processes set and add proc there so we always keep a reference to it. Now, the program works as intended. However, it still has an issue: we are leaking QProcess objects.

While in this case the leak is very short­-lived, since we are end­ing the pro­gram right af­ter the process end­s, in a re­al pro­gram this is not a good idea.

So, we would need to add a way to remove proc from processes in finished. This is not as easy as it may seem. Here is an idea that will not work as you expect:

def launch_process():
    # Do something asynchronously
    proc = QtCore.QProcess()
    processes.add(proc)
    proc.start("/bin/sleep 3")
    # Remove the process from the global set when done
    proc.finished.connect(lambda: processes.remove(proc))
    # After it finishes, call finished
    proc.finished.connect(finished)

In this version, we will still leak proc, even though processes is empty! Why? Because we are keeping a reference to proc in the lambda!

I don't really have a good answer for that that doesn't involve turning everything into members of a QObject and using sender to figure out what process is ending, or using QSignalMapper. That version is left as an exercise.

Diego Sarmentero / 2012-02-11 02:22:

Algo que me pasó al principio, es cuando se crean ventanas que se quieren mostrar, pero se asignan a variables que no existen fuera del scope del metodo tambien y por ahi te matas debugueando de por que la ventana no se abre, y en realidad se esta abriendo y destruyendo antes de que te des cuenta :P

Tobias Sargeant / 2012-02-12 16:17:


    hold=[proc]
    proc.finished.connect(finished)
    proc.finished.connect(lambda: hold.pop() )

If you do this, then you don't need to have the global processes set, either.

Roberto Alsina / 2012-02-13 00:53:

Good idea!

I have not tested it, but there is an additional problem that may be an issue here: the order of execution of the slots is not guaranteed. If the lambda is called before finished, proc will be GCd before finished is called, which may cause problems depending on what finished does.

Tobias Sargeant / 2012-02-13 15:15:

I guess in that case you could write:


hold = [proc]
def callback():
  finished()
  hold.pop()
proc.finished.connect(callback)

to enforce the call order. Alternatively you could rely on the cyclic garbage collector in 2.7+ to collect the reference cycle created by the lambda. However it may be that, because the reference to the lambda is held by PyQt, the traversal requirements for the collector aren't satisfied in this case, and the cycle may be uncollectable.