So I am working on a Tkinter application, the structure is somehow complex and there are often some cyclic references between child frames and parent frames or different objects.
Python 2.7 and versions before 3.4 do not collect objects that are part of a reference cycle when one of them has a __del__
method, after python 3.4 the interpreter tries harder but there are still some cases where it does not work (see this example)
At some point Tkinter Variables are used (StringVar and IntVar only).
These classes have a __del__
method, therefore when they are part of a cycle of references none of the objects in the cycle are collected by the garbage collector.
Here is a minimal reproductible example with pyobjgraph to show the presence of the objects in memory (you will need Tkinter, pyobjgraph and dot installed to run this).
try :
import Tkinter as tk
except :
import tkinter as tk
class ParentWindow(tk.Frame):
def __init__(self, root):
self.intvarframes = []
self.root = root
self.spawn = tk.Button(root, text="spawn", command=lambda :self.intvarframes.append(FrameWithIntVar(self)))
self.remove = tk.Button(root, text="remove", command=lambda :self.intvarframes.pop().remove())
self.spawn.pack()
self.remove.pack()
def tryme(self, child):
print "child"+str(child)
class FrameWithIntVar:
def __init__(self, parent):
self.parent = parent
self.frame = tk.Frame(self.parent.root)
self.entry = tk.IntVar(self.frame)
self.entry.trace("w", lambda e : self.parent.tryme(self))
self.frame.pack()
self.bigobj = MyVeryBigObject()
c = tk.Checkbutton(self.frame, text="cb", variable=self.entry)
c.pack()
def remove(self):
self.frame.destroy()
#del self.entry
class MyVeryBigObject:
def __init__(self):
self.values = list(range(10**4))
root = tk.Tk()
ParentWindow(root)
root.mainloop()
import objgraph
if objgraph.by_type("MyVeryBigObject"):
objgraph.show_backrefs(objgraph.by_type("MyVeryBigObject"), max_depth=10, filename="test.dot")
from subprocess import check_call
check_call(['dot', '-Tpng', 'test.dot', '-o', 'test.png'])
else :
print ("No MyVeryBigObject in memory")
To demonstrate, just launch the application, spawn a few checkboxes, destroy them and close the application, then open the test.png image.
As you can see there are as many instances of MyVeryBigObject
as you created checkboxes.
Here the cyclic reference happen because the lambda self.parent.tryme(self)
captures self
(twice).
When I uncomment the del self.entry
in the remove
method. The objects are freed correctly.
Note that this is a simple example and in the actual application I would have to manually propagate the destruction of a frame to all of its children to make sure I destroy all the variables. While it could work, it would mean more code, more maintenance and possibly common errors that comes with manual memory management.
So the question is : Is there a simpler way to do this ?
Maybe there is a way to be noticed by tkinter when a frame is destroyed or a way to use Tkinter variables without a __del__
method but I didn't find anything yet.
Thank you in advance