Search code examples
pythonoverridingdecoratordel

Why is `__del__` method not called if an instance is stored?


To deepen my understanding of decorators I tried to come up with an alternative solution for the count_instances decorator in Tonie Victor's blog (section Common Use Cases of Class Decorators).

Instead of using a function, I use a class as a decorator and I extend the functionality to keep a proper count even when instances of the decorated class are deleted. For that I decorate the __del__ method of the decorated class.

In fact this works as expected on my simple test cases. However, when I try to change the decorator from an instance counter to an instance tracker that contains a list of all instances, it all of a sudden does not work as expected anymore.

More specifically, if I add the instance of the decorated class to a list in the InstanceCounter class, the (decorated) __del__ method isn't called anymore if I delete an instance and the counts are wrong.

Note that I have not implemented all functionality for the tracker yet. I planned to

  1. replace the _count attribute with a property that returns the length of _instances;

  2. add a self._instances.pop(self._instances.index(obj)) to wrapper.

class InstanceCounter:
    def __init__(self, cls):
        self._instances = []
        self._count = 0
        if hasattr(cls, "__del__"):
            del_meth = cls.__del__
        else:
            del_meth = None
        cls.__del__ = self._del_decorator(del_meth)
        self._wrapped_cls = cls

    @property
    def instance_count(self):
        return self._count

    def __call__(self):
        self._count += 1
        obj = self._wrapped_cls()
        # self._instances.append(obj)        # When this line is uncommented...
        return obj

    def _del_decorator(self, del_method):
        def wrapper(obj):
            self._count -= 1
            if del_method:
                del_method(obj)

        return wrapper


@InstanceCounter
class MyClass:
    def __init__(self):
        pass

    def __del__(self):                  # ... this method will not be executed.
        print(f"Deleting object of class {__class__}")


@InstanceCounter
class OtherClass:
    def __init__(self):
        pass

# Test cases with expected output. 
my_a = MyClass()
print(f"{MyClass.instance_count=}")  # MyClass.instance_count=1

my_b = MyClass()
print(f"{MyClass.instance_count=}")  # MyClass.instance_count=2

del my_a
print(f"{MyClass.instance_count=}")  # MyClass.instance_count=1

oth_c = OtherClass()
print(f"{OtherClass.instance_count=}")  # OtherClass.instance_count=1

oth_d = OtherClass()
print(f"{OtherClass.instance_count=}")  # OtherClass.instance_count=2

del oth_c
print(f"{OtherClass.instance_count=}")  # OtherClass.instance_count=1

del oth_d
print(f"{OtherClass.instance_count=}")  # OtherClass.instance_count=0

Why is the __del__ method not called if an instance of the class is saved in the decorating class? How can I fix this?


Solution

  • After uncommenting self._instances.append(obj), my_a = MyClass() creates two references to the object - my_a and MyClass._instances[0]. del my_a deletes only the first reference, so the object is not destroyed and, thus, __del__ method is not called.

    If this is not what you want, you can store a list of weak references instead of normal references. To do this, just replace self._instances.append(obj) with self._instances.append(weakref.ref(obj)).