Search code examples
pythonpython-3.5deep-copyweak-references

Creating a deepcopy of class instance with nested weakref to it


I have two classes: a parent class and a container class. The parent class instance has matching container class instance as a weak reference.

There is a problem while deep copying the parent instance, the weakref is still linking to the original instance. Here is a minimal example:

import weakref
from copy import deepcopy


class Container:    
    def __init__(self, parent):
        self.parent = weakref.ref(parent)


class Parent:
    def __init__(self):
        self.container = Container(self)


if __name__ == '__main__':
    parent1 = Parent()
    assert(parent1 is parent1.container.parent())

    parent2 = deepcopy(parent1)
    assert(parent2 is parent2.container.parent())

The second assertion fails.

I suspect that the __deepcopy__ magic method can be implemented, but not sure how exactly.


Solution

  • The problem is that deepcopy won't follow the weakref.ref link. It doesn't even copy the weakref.ref:

    >>> from copy import deepcopy
    >>> import weakref
    >>>
    >>> parent1 = Parent()
    >>> ref1 = weakref.ref(parent1)
    >>> ref2 = deepcopy(ref1)
    >>> ref1 is ref2
    True
    

    That is explicitly hardcoded in the copy module. I don't know why this is but I suspect they had their reasons.

    However you can implement a __deepcopy__ method:

    import weakref
    from copy import deepcopy
    
    class Container:    
        def __init__(self, parent):
            self.parent = weakref.ref(parent)
    
    class Parent:
        def __init__(self):
            self.container = Container(self)
    
        def __deepcopy__(self, memo):
            # set __deepcopy__ element to "false"-ey value so we don't go into
            # recusion.
            self.__deepcopy__ = None
            try:
                new = deepcopy(self, memo)
            finally:
                # Always delete the self.__deepcopy__ again, even if deepcopying failed
                del self.__deepcopy__
            del new.__deepcopy__  # remove the copied __deepcopy__ attribute
            new.container.parent = weakref.ref(new)
            return new
    
    if __name__ == '__main__':
        parent1 = Parent()
        assert parent1 is parent1.container.parent()
    
        parent2 = deepcopy(parent1)
        assert parent2 is parent2.container.parent()
    
        parent3 = deepcopy(parent2)
        assert parent3 is parent3.container.parent()
    

    It's a bit ugly because of the temporary __deepcopy__ instance attribute. But it allows to use the normal deepcopy function on self without going into infinite recursions and then you only have to manually create a new weak reference to your parent.

    You could even do it without temporarily setting __deepcopy__ (it should work correctly):

    import weakref
    from copy import deepcopy
    
    class Container:    
        def __init__(self, parent):
            self.parent = weakref.ref(parent)
    
    class Parent:
        def __init__(self):
            self.container = Container(self)
    
        def __deepcopy__(self, memo):
            # Create a new class
            new = object.__new__(type(self))
            memo[id(self)] = new   # add the new class to the memo
            # Insert a deepcopy of all instance attributes
            new.__dict__.update(deepcopy(self.__dict__, memo))
            # Manually update the weakref to be correct
            new.container.parent = weakref.ref(new)
            return new
    
    if __name__ == '__main__':
        parent1 = Parent()
        assert parent1 is parent1.container.parent()
    
        parent2 = deepcopy(parent1)
        assert parent2 is parent2.container.parent()
    
        parent3 = deepcopy(parent2)
        assert parent3 is parent3.container.parent()