Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

PyQt Quickie: Que no te lleve el basurero

Qt tie­ne sus me­ca­nis­mos pa­ra crear y eli­mi­nar ob­je­tos (el ár­bol de QOb­jec­ts, smart poin­ter­s, etc.) y Py­Qt usa Py­tho­n, así que tie­ne gar­ba­ge co­llec­tio­n.

Con­si­de­re­mos un ejem­plo sim­ple:

from PyQt4 import QtCore

def finished():
    print "El proceso termino!"
    # Salir de la aplicación
    QtCore.QCoreApplication.instance().quit()

def launch_process():
    # Hacer algo asincrono
    proc = QtCore.QProcess()
    proc.start("/bin/sleep 3")
    # Cuando termine, llamar a finished
    proc.finished.connect(finished)

def main():
    app = QtCore.QCoreApplication([])
    # Lanzar el proceso
    launch_process()
    app.exec_()

main()

Si eje­cu­tás eso, te va a pa­sar es­to:

QProcess: Destroyed while process is still running.
El proceso termino!

Encima el script no termina nunca. ¡Diversión! El problema es que proc está siendo borrado al final de launch_process porque no hay más referencias a él.

És­ta es una me­jor ma­ne­ra de ha­cer­lo:

from PyQt4 import QtCore

processes = set([])

def finished():
    print "El proceso termino!"
    # Salir de la aplicación
    QtCore.QCoreApplication.instance().quit()

def launch_process():
    # Hacer algo asincrono
    proc = QtCore.QProcess()
    processes.add(proc)
    proc.start("/bin/sleep 3")
    # Cuando termine, llamar a finished
    proc.finished.connect(finished)

def main():
    app = QtCore.QCoreApplication([])
    # Lanzar el proceso
    launch_process()
    app.exec_()

main()

Al agregar un processes global y meter ahí proc, mantenemos siempre una referencia, y el programa funciona. Sin embargo, sigue teniendo un problema: nunca eliminamos los objetos QProcess.

Si bien en es­te ca­so la pér­di­da de me­mo­ria es muy bre­ve por­que el pro­gra­ma ter­mi­na en­se­gui­da, en un pro­gra­ma de ver­dad es­to no es bue­na idea.

Así que necesitamos agregar una manera de sacar proc de processes cuando no lo necesitemo. Esto no es tan fácil como parece. Por ejemplo, esto no funciona bien:

def launch_process():
    # Hacer algo asincrono
    proc = QtCore.QProcess()
    processes.add(proc)
    proc.start("/bin/sleep 3")
    # Sacamos el proceso del global cuando no lo necesitamos
    proc.finished.connect(lambda: processes.remove(proc))
    # Cuando termine, llamar a finished
    proc.finished.connect(finished)

¡En esta versión, todavía tenemos un memory leak de proc, aunque processes esté vacío! Lo que pasa es que el lambda contiene una referencia a proc.

No tengo una my buena respuesta para este problema que no involucre convertir todo en miembros de un Qbject y usar sender para saber cuál proceso es el que termina, o usar QSignalMapper. Esa versión la dejo como ejercicio para el lector ;-)

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.


Contents © 2000-2023 Roberto Alsina