PyQt Quickie: Que no te lleve el basurero
Qt tiene sus mecanismos para crear y eliminar objetos (el árbol de QObjects, smart pointers, etc.) y PyQt usa Python, así que tiene garbage collection.
Consideremos un ejemplo simple:
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 ejecutás eso, te va a pasar esto:
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.
Ésta es una mejor manera de hacerlo:
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 este caso la pérdida de memoria es muy breve porque el programa termina enseguida, en un programa de verdad esto no es buena 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 ;-)
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
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.
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.
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.