0

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

P bertJoha
  • 96
  • 2
  • 10
  • When a frame is destroyed everything in that frame is also destroyed so I don't think you should have any problems with memory here. I believe its automatically managed. – Mike - SMT Jul 30 '18 at 21:28
  • After some more research it seems that my problem is solved by [PEP 442](https://www.python.org/dev/peps/pep-0442/) but I am stuck with python 2.7 and before python 3.4 the interpreter will not call destructors on objects that are part of a reference cycle `and` have a `__del__` method (see [this answer](https://stackoverflow.com/a/33103247/5840759)) – P bertJoha Jul 31 '18 at 08:11

1 Answers1

0

Python 2 or Python 3 < 3.4

There is a Destroy event in Tkinter that is called when a children frame is destroyed so I just have to add this to every frame that contains a Tkinter Variable

self.frame.bind("<Destroy>", self.remove)

With a remove method that deletes the entry and all objects that have references to it in my current object.

Python >= 3.4

So after PEP 442 it seems that the interpreter no longer has difficulties dealing with this particular case but I have not tried it so if someone could confirm that no objects are leaked when running my example in Python >3.4 that would be great.

Note that while the interpreter tries harder to free the objects there are still some cases where it does not work (see this example)

Community
  • 1
  • 1
P bertJoha
  • 96
  • 2
  • 10