With the following program:
from traits.api import HasTraits, Int, Instance
from traitsui.api import View
class NewView(View):
def __del__(self):
print('deleting NewView')
class A(HasTraits):
new_view = Instance(NewView)
def __del__(self):
print('deleting {}'.format(self))
a = Int
def default_traits_view(self):
new_view = NewView('a')
return new_view
running
a = A()
del(a)
returns
deleting <__main__.A object at 0x12a016a70>
as it should.
If I do
a = A()
a.configure_traits()
and after closing the dialog:
del(a)
I have the same kind of message:
deleting <__main__.A object at 0x12a016650>
with no mention of the NewView being deleted.
In geneal, what are the good practices to avoid memory leaks with Traits and TraitsUI?
What's going on here is that the NewView
object is involved in a reference cycle, and the objects in that cycle don't get automatically collected as part of CPython's primary reference-counting-based object deallocation mechanism. However, they should be eventually collected as part of CPython's cyclic garbage collector, or you can force that collection by doing a gc.collect()
, so there should be no actual long-term memory leak here.
Ironically, attempting to detect that eventual collection by adding a __del__
method to NewView
hinders the process, since it renders the NewView
object uncollectable: at least in Python 2, Python won't try to collect cycles containing objects that have __del__
methods. See the gc
docs for details. (Python 3 is somewhat cleverer here, thanks to the changes outlined in PEP 442.) So with the __del__
method, using Python 2, there will indeed be a slow memory leak over time. The solution is to remove the __del__
method.
Here's a graph showing the reference cycle (actually, this shows the whole strongly-connected component of the object graph containing the NewView
object): nodes are the objects involved, and arrows go from referrers to referents. In the bottom right portion of the graph, you see that the NewView
object has a reference to its top-level Group
(via the content
attribute), and that Group
object has a reference back to the original view (the container
attribute). There are similar cycles going on elsewhere in the view.
It's probably worth opening a feature request on the Traits UI tracker: in theory, it should be possible to break the reference cycles manually when the view is no longer needed, though in practice that might require significant reworking of the Traits UI source.
Here's some code that demonstrates that (with the __del__
methods removed) a call to gc.collect
does collect the NewView
object: it stores a weak reference to the view on the A
instance, with a callback that reports when that view is garbage collected.
from traits.api import HasTraits, Int, Instance
from traitsui.api import View
import gc
import weakref
class NewView(View):
pass
def report_collection(ref):
print("NewView object has been collected")
class A(HasTraits):
a = Int
def default_traits_view(self):
new_view = NewView('a')
self.view_ref = weakref.ref(new_view, report_collection)
return new_view
def open_view():
a = A()
a.configure_traits()
print("Collecting cyclic garbage")
gc.collect()
print("Cyclic garbage collection complete")
On my machine, here's what I see when open_view
is called:
>>> open_view()
Collecting cyclic garbage
NewView object has been collected
Cyclic garbage collection complete