Search code examples
pythontkintertrace

Tk Python - .trace leaves reference to object and doesnt allow the object to be garbage collected


the basics of my problem is this, I have an object which gets created and destroyed fine (i.e. garbage collected) until I introduce the below line:

self.variableA.trace("w", self.printSomeStuff)

variableA is a stringVar and is set up as shown below

self.variableA = tk.StringVar(self.detailViewHolderFrame)
self.variableA.set(self.OPTIONS0[0])

for some reason this line causes there to be a reference to the object and hence it never gets garbage collected and I end up with a memory leak.

Does anyone have any ideas on how I might get round this other than using a different widget.

I can expand on the code further but first I wanted to see if this is a common issue and if anybody know the underlying problem.


Solution

  • You can try to delete the callback(s) using trace_vdelete(). Hopefully that will remove the reference(s) to the tk variable.

    def cb(*args):
        print args
    
    v = StringVar()
    v.trace('r', cb)
    v.trace('w', cb)
    v.set(10)   # cb called
    # ('PY_VAR5', '', 'w')
    v.get()     # cb called
    # ('PY_VAR5', '', 'r')
    # '10'
    print v.trace_vinfo()
    # [('w', '139762300731216cb'), ('r', '139762505534512cb')]
    for ti in v.trace_vinfo():
        print "Deleting", ti
        v.trace_vdelete(*ti)
    # Deleting ('w', '139762300731216cb')
    # Deleting ('r', '139762505534512cb')
    print v.trace_vinfo()
    #
    

    You will need to figure out how to call trace_vdelete(), possibly you can do that from the __del__() method of the tk variable by subclassing and overriding __del__():

    class MyStringVar(StringVar):
        def __del__(self):
            for t in self.trace_vinfo():
                self.trace_vdelete(*t)
            StringVar.__del__(self)
    

    Update

    As Blckknght pointed out, __del__ will not be called until just before the object is garbage collected, and this will not happen when the StringVar is being traced because an additional reference is held.

    One solution is to use an external function as the trace callback.

    If, however, you want the trace callback to be a method of your class, a solution is to set your class up as a context manager, i.e. a class that can be used in a with statement. Then a cleanup method can be called when the with statement terminates. The cleanup method will delete the trace callbacks and this will remove the reference to your object from the tk trace system. It should then be available for garbage collection. Here's an example:

    import Tkinter as tk
    
    root = tk.Tk()
    
    class YourClass(object):
        def __init__(self):
            self.s = tk.StringVar()
            self.s.trace('w',self.cb)
            self.s.trace('r',self.cb)
    
        def __enter__(self):
            """Make this class usable in a with statement"""
            return self
    
        def __exit__(self, exc_type, exc_value, traceback):
            """Make this class usable in a with statement"""
            self.cleanup()
    
        def __del__(self):
            print 'YourClass.__del__():'
    
        def cb(self, *args):
            print 'YourClass.cb(): {}'.format(args)
    
        def cleanup(self):
            print 'YourClass.cleanup():'
            for t in self.s.trace_vinfo():
                print 'YourClass.cleanup(): deleting {}'.format(t)
                self.s.trace_vdelete(*t)
    

    Demo

    >>> obj = YourClass()
    >>> obj.s.set('hi')
    YourClass.cb(): ('PY_VAR5', '', 'w')
    >>> obj.s.get()
    YourClass.cb(): ('PY_VAR5', '', 'r')
    'hi'
    >>> obj.s.trace_vinfo()
    [('r', '139833534048848cb'), ('w', '139833534047728cb')]
    >>> del obj
    >>> 
    

    N.B. YourClass.__del__() was not called because the trace system still holds a reference to this instance of YourClass. You could manually call the cleanup() method:

    >>> obj = YourClass()
    >>> obj.cleanup()
    YourClass.cleanup():
    YourClass.cleanup(): deleting ('r', '139833533984192cb')
    YourClass.cleanup(): deleting ('w', '139833533983552cb')
    >>> del obj
    YourClass.__del__():
    

    Calling cleanup() removes the references to the YourClass instance and garbage collection can occur. It's easy to forget to call cleanup() every time, and it's a pain too. Using the context manager makes calling clean up code easy:

    >>> with YourClass() as obj:
    ...     obj.s.set('hi')
    ...     obj.s.get()
    ...     obj.s.trace_vinfo()
    ... 
    YourClass.cb(): ('PY_VAR2', '', 'w')
    YourClass.cb(): ('PY_VAR2', '', 'r')
    'hi'
    [('r', '139833534001392cb'), ('w', '139833533984112cb')]
    YourClass.cleanup():
    YourClass.cleanup(): deleting ('r', '139833534001392cb')
    YourClass.cleanup(): deleting ('w', '139833533984112cb')
    

    Here __exit__() was called automatically upon exit from the with statement and it delegates to cleanup(). Deleting or rebinding obj will cause garbage collection, as will the variable going out of scope for example upon return from a function:

    >>> obj = None
    YourClass.__del__():
    

    One final benefit of using the context manager is that __exit()__ will always be called upon exit from the context manager, for whatever reason. This includes any unhandled exceptions, so your clean up code will always be called:

    >>> with YourClass() as obj:
    ...     1/0
    ... 
    YourClass.cleanup():
    YourClass.cleanup(): deleting ('r', '139833395464704cb')
    YourClass.cleanup(): deleting ('w', '139833668711040cb')
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
    ZeroDivisionError: integer division or modulo by zero
    >>> del obj
    YourClass.__del__():