enthought / traits

Observable typed attributes for Python classes
Other
438 stars 85 forks source link

sync_traits creates reference cycles. #69

Open mdickinson opened 11 years ago

mdickinson commented 11 years ago

The sync_traits method on HasTraits objects creates reference cycles. In the script below, the create_cycles function creates two cycles that won't get collected until the cyclic garbage collector runs. See the attached image for the details of the cycles.

import gc

import refcycle

from traits.api import HasStrictTraits, HasTraits, Instance, Str

class Eggs(HasStrictTraits):
    spam = Str

class Sandwich(HasStrictTraits):
    spam = Str

def create_cycles():
    sandwich = Sandwich()
    eggs = Eggs()
    sandwich.sync_trait('spam', eggs)

def main():
    gc.disable()
    gc.collect()
    create_cycles()
    garbage = refcycle.garbage()
    print "garbage: ", garbage
    with open('garbage.gv', 'w') as f:
        f.write(garbage.to_dot())

if __name__ == '__main__':
    main()

garbage

jvkersch commented 10 years ago

The problem seems to be that when the trait synchronization is set up (in https://github.com/enthought/traits/blob/master/traits/has_traits.py#L2652), the weakref callback is a member function (it doesn't matter whether the callback is a member of the object itself, or of the object to be synched with, but the former produces slightly nicer-looking cycles). See e.g. the following snippet:

import gc
import weakref
import refcycle

class A(object):

    def baby_sync(self, obj, mutual=True):
        self.ref = weakref.ref(obj, obj.callback)
        if mutual:
            obj.baby_sync(self, mutual=False)

    def callback(self, ref):
        pass

def create_cycles():
    a = A()
    b = A()
    a.baby_sync(b)

def main():
    gc.disable()
    gc.collect()
    create_cycles()
    garbage = refcycle.garbage()
    print "garbage: ", garbage
    with open('garbage.gv', 'w') as f:
        f.write(garbage.to_dot())

if __name__ == '__main__':
    main()

garbage