Destroying a Singleton object in Python
Asked Answered
S

2

13

I have a Singleton object in Python:

class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

    @classmethod
    def destroy(cls):
        del cls._instances[cls]


class MockObject(metaclass=Singleton):

    def __init__(self, *args, **kwargs):
        # various things

I would like to destroy the object at some point, so I wrote a classmethod in the metaclass. However, the cls refers to the metaclass Singleton rather than MockObject. Is there a way to call the destroy function with a value of MockObject?

Silencer answered 25/4, 2017 at 19:51 Comment(9)
Put the destroy method on MockObject, where it belongs (or make a MockBase that does this). But there's no point in trying to destroy the instance anyway; if there is any other reference to it anywhere, it will stay alive.Madison
Why not just define a __del__ method on the metaclass?Toplevel
@Madison what would a destroy method on MockObject look like? I know for sure that there are no other references to this object at this point in the code.Silencer
@MadPhysicist I would like to deterministically delete the object in order to ensure there is no contamination of state between different segments of the program.Silencer
As @Madison points out, you are actually going to introduce a whole bunch of contamination by doing this. You will be deleting the the reference in your dictionary, but not any lingering references that your program has elsewhere. Now you will end up with multiple instances of your singleton floating around. You may want to read up on how objects are really deleted in Python and rethink your approach.Toplevel
Something like del Singleton._instances[cls] maybe. You could also use the gc module to make sure there's only one reference and throw an error otherwise.Madison
@MadPhysicist Like I said, there are no more references to this object at this point. I just want to enforce a deletion of the information so that I can create another fresh object later.Silencer
@Madison that's a good idea re: gcCroon
Also, have you considered a simpler approach, where you just redefine __new__ on the metaclass instead of __call__, which should call both __new__ and __init__? That's basically what bool and NoneType do in Python.Toplevel
T
27

Instead of defining a custom method for deleting the instance reference use a WeakValueDictionary.

Now when there are no more references of MockObject anywhere it will be cleaned up from Singleton._instances automatically.

from weakref import WeakValueDictionary


class Singleton(type):
    _instances = WeakValueDictionary()

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            # This variable declaration is required to force a
            # strong reference on the instance.
            instance = super(Singleton, cls).__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]


class MockObject(metaclass=Singleton):

    def __init__(self, *args, **kwargs):
        pass


if __name__ == '__main__':
    m = MockObject()
    print(dict(Singleton._instances))
    del m
    print(dict(Singleton._instances))

Output:

{<class '__main__.MockObject'>: <__main__.MockObject object at 0x104531128>}
{}
Tabling answered 25/4, 2017 at 20:9 Comment(0)
L
0

destroy() as Singleton's classmethod

class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

    @classmethod
    def destroy(metacls, cls):
        if cls in metacls._instances:
            del metacls._instances[cls]


class MockObject(metaclass=Singleton):
    def __init__(self, *args, **kwargs):
        pass


if __name__ == '__main__':
    m = MockObject()
    print(Singleton._instances)
    
    # call `destroy()` at metaclass level with subclass as argument
    Singleton.destroy(MockObject)
    print(Singleton._instances)
    
    # when called after destroy() 
    # a new instance is created at a different memory location
    m = MockObject()
    print(Singleton._instances)

Output:

{<class '__main__.MockObject'>: <__main__.MockObject object at 0x1004b6660>}
{}
{<class '__main__.MockObject'>: <__main__.MockObject object at 0x1004b6690>}

destroy() as subclass's classmethod

Simply without @classmethod:

class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

    def destroy(cls):
        # NOTE: cls._instances is reference to Singleton._instances
        if cls in cls._instances:
            del cls._instances[cls]


class MockObject(metaclass=Singleton):
    def __init__(self, *args, **kwargs):
        pass


if __name__ == '__main__':
    m = MockObject()
    print(Singleton._instances)

    # call `destroy()` at subclass level
    MockObject.destroy()
    print(Singleton._instances)
    
    m = MockObject()
    print(Singleton._instances)

Output:

{<class '__main__.MockObject'>: <__main__.MockObject object at 0x104696600>}
{}
{<class '__main__.MockObject'>: <__main__.MockObject object at 0x104696630>}

The classes defined use metaclass are instances of metaclass, so the instance method of metaclass are inherited as classmethod of regular class, i.e. classmethod of MockObject is instance method of Singleton.

destroy() as subclass's instance method

If you need define a destroy method in MockObject's instances, you'd have to operate in the __new__ method of metaclass.

class Singleton(type):
    _instances = {}

    def __new__(cls, name, bases, namespace):
        namespace.update(destroy=Singleton.destroy)
        return super().__new__(cls, name, bases, namespace)

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

    @staticmethod
    def destroy(self):
        cls = self.__class__
        if cls in cls._instances:
            del cls._instances[cls]


class MockObject(metaclass=Singleton):
    def __init__(self, *args, **kwargs):
        pass


if __name__ == '__main__':
    m = MockObject()
    print(Singleton._instances)
    
    # call `destroy()` at instance level
    m.destroy()
    print(Singleton._instances)

    m = MockObject()
    print(Singleton._instances)

Output:

{<class '__main__.MockObject'>: <__main__.MockObject object at 0x10431aa20>}
{}
{<class '__main__.MockObject'>: <__main__.MockObject object at 0x10431aa50>}
Landsknecht answered 28/5, 2024 at 12:44 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.