[PyQt] Virtual methods and an extra reference
Sundance
sundance at ierne.eu.org
Thu Jun 19 10:59:06 BST 2008
Kevin Watters wrote:
> I'm tracking down a memory leak in my app--and I think it's boiling
> down to a virtual method on one of my classes that has an extra
> reference, one not coming from any Python object.
Hi Kevin, hi Phil, hi all,
Okay, Kevin, you MUST be reading my mind because, first thing I did this
morning after yesterday evening's work was fire up my email client and
start a message entitled "Serious memory leak in PyQt?".
I can confirm the issue and, perhaps, provide a little more data.
The core issue is the way Python handles references around bound
methods.
Let obj.meth() be a method on an object. By default, the reference count
to the bound method is zero, and the reference count to the object is,
exactly, the number of external references you have to that object.
I am not sure how it is that the refcount to the bound method is zero by
default. I suspect it may have to do with the way methods are
implemented in Python; i.e. the method is not on the object but on
the /class/, and the method is only bound to the /object/ at call time.
Let us now extract the bound method under an external label:
m = obj.meth
The method object returned by the implicit getattr is /bound/: it comes
with a closure that contains the object the method is bound to, and
that object's refcount thus increases by one.
And here's the issue: what PyQt (well, SIP, really) seems to do with
virtual methods that are reimplemented in Python, is grab a reference
to the bound method, and /never let it go/.
Hence, your bound method's refcount never returns to zero. Hence,
your /object's/ refcount never returns to zero. And you get a memory
leak.
This is an issue because all the event management in Qt is done with
virtual methods.
Example code:
---[ Code ]------------------------------------------------------------
import sys
from PyQt4 import QtGui
app = QtGui.QApplication( sys.argv )
class MyWidget( QtGui.QWidget ):
def resizeEvent( s, e ):
print "In resizeEvent."
return QtGui.QWidget.resizeEvent( s, e )
def __del__( s ):
print "%s deleted." % s
class MyOtherWidget( QtGui.QWidget ):
def nonVirtualMethod( s ):
print "All's good."
def __del__( s ):
print "%s deleted." % s
## Let us now test all the possible cases.
obj1 = MyOtherWidget()
obj1.nonVirtualMethod()
del obj1 ## No virtual method, no problem.
obj2 = MyWidget()
del obj2 ## Virtual method never used, no problem.
obj3 = MyWidget()
obj3.show() ## resizeEvent is bound by SIP.
app.processEvents()
obj3.close()
del obj3 ## ... But never released. Memory leak.
-----------------------------------------------------------------------
Usually this wouldn't be a huge issue, but when you are creating and
destroying lots of widgets, suddenly it's a thorny problem.
Phil, may we please discuss possible ways to fix this? It might be as
simple as using weak references to the bound methods instead of direct
references.
Arguably, 'simple' is perhaps something of a misnommer, because Python's
weakrefs don't handle bound methods well at all (the weak reference
dies the moment the bound method goes out of scope).
Fortunately, that can be worked around reasonably simply:
---[ Code ]------------------------------------------------------------
import weakref
class WeakCallableRef:
def __init__( s, fn, callback=None ):
assert callable( fn )
s._methname = None
s._objref = None
s._fnref = None
s._dead = False
s._callback = callback
obj = getattr( fn, "im_self", None )
if obj: ## fn is a bound method
s._objref = weakref.ref( obj, s.markDead )
s._methname = fn.im_func.func_name
else: ## fn is a static method or a plain function
s._fnref = weakref.ref( fn, s.markDead )
def markDead( s, ref ):
s._dead = True
if s._callback: s._callback( s )
def __call__( s ):
if s._dead: return None
if s._objref: ## bound method
return getattr( s._objref(), s._methname, None )
else:
return s._fnref()
-----------------------------------------------------------------------
This class behaves like weakref.ref, only for callables.
Might it be possible to have it used by PyQt for virtual methods?
Thanks,
-- S.
More information about the PyQt
mailing list