I've discovered a memory leak associated with event callbacks on items
in a Tkinter Canvas widget. It seems that deletecommand() is not
being called to free references when an item is deleted.
I also have a bugfix. Tk conveniently keeps track of the callbacks
for an item, and you can obtain this list and parse it for command
names to pass to deletecommand(). This makes the fix very simple.
At about line 1190 in Tkinter.py, in the definition of the Canvas class:
def delete(self, *args):
self._do('delete', args)
needs to be changed to:
def delete(self, *args):
for tag in args:
for a in _string.split(self.tag_bind(tag)):
b=self.tag_bind(tag, a)
c=_string.split(b, '[')[1]
d=_string.split(c)[0]
self.deletecommand(d)
self._do('delete', args)
This bug might also be present in the tag_delete method of the Text
widget since the implementation is similar, but I haven't had time to
take a look.
I discovered this in the course of writing a tree control that uses a
"node" helper class, which keeps track of the node icon, label, and
expansion state, among other things. My tree code is too long to
post, but the following will serve as a simplified example:
---cut here---
from Tkinter import *
# helper class
class node:
def __init__(self, master, x, y):
self.master=master
self.oval = self.master.create_oval(x, y, x+20, y+20, fill="green")
self.master.tag_bind(self.oval, "<Any-Enter>", self.mouseEnter)
self.master.tag_bind(self.oval, "<Any-Leave>", self.mouseLeave)
self.master.tag_bind(self.oval, "<B1-Motion>", self.mouseMove)
self.master.tag_bind(self.oval, "<1>", self.mouseDown)
self.master.tag_bind(self.oval, "<2>", self.mouseKill)
def __del__(self):
print 'killed'
def mouseEnter(self, event):
self.master.itemconfig(self.oval, fill="red")
def mouseLeave(self, event):
self.master.itemconfig(self.oval, fill="blue")
def mouseDown(self, event):
# remember where the mouse went down
self.lastx = event.x
self.lasty = event.y
def mouseMove(self, event):
self.master.move(self.oval, event.x - self.lastx, event.y - self.lasty)
self.lastx = event.x
self.lasty = event.y
def mouseKill(self, event):
self.master.delete(self.oval)
# main widget
class Test(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
b=Button(self, text='QUIT', foreground='red', command=self.quit)
b.pack(side=LEFT, fill=BOTH)
self.draw = Canvas(self, width="5i", height="5i")
self.draw.pack(side=LEFT)
node(self.draw, 20, 20)
node(self.draw, 80, 20)
node(self.draw, 20, 80)
node(self.draw, 80, 80)
root=Tk()
Test(root).pack()
root.mainloop()
---cut here---
With the buggy version of Tkinter, the __del__ method never gets
called, and you never get the "killed" message. Printing
sys.getrefcount(self) in mouseKill() reveals a reference for each of
the callbacks (aha!!) plus the temporary reference. After the patch
is applied, everything works properly.
P.S. thank you, Guido, for Python. I've learned OOP from it without
being exposed to the brokenness of C++ and the learning curve is
incredible. I can remember several months ago when I couldn't even
figure out how to use Tkinter and now I'm contributing patches!