Singleton

Singleton implementation

import random
class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        print('enter Singleton call')
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        print('exit Singleton call')
        return cls._instances[cls]

class Baseclass:
    def __new__(cls, *args, **kwargs):
        print('Base new')
        return super().__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        print('Base init')
        super().__init__(*args, **kwargs)

class Foo(Baseclass, metaclass=Singleton):
    def __new__(cls, *args, **kwargs):
        print('Foo new')
        return super().__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        print('Foo init')
        self.value = random.randint(0,10)
        super().__init__(*args, **kwargs)

a = Foo()
enter Singleton call
Foo new
Base new
Foo init
Base init
exit Singleton call

Note

  • The order of function call is deeply embedded in type’s call mentioned in section Python create sequence of this document and also in this C code.

  • The *args and **kwargs of Foo’s __new__ will unchangely pass to Foo’s __init__ implicitly.

  • _instances = {} instead of _instances = None make Singleton available for multiple classes instead only one. Therefore, you could have Foo(metaclass=Singleton), Bar(metaclass=Singleton) … instead of only one.

Another Singleton implementation by using __new__ case and why it’s not work

import random
class Singleton:
    _ins = None
    def __new__(cls, tmp, *args, **kwargs):
        if not cls._ins:
            cls._ins = super().__new__(cls,*args)
            return cls._ins
        else:
            return cls._ins

class Foo(Singleton):
    def __init__(self):
        self.value = random.randint(0,10)
a = Foo()
print(a.value)
b = Foo()
print(a is b)
print(b.value)

Output:

0
True
4

The reason is __new__ and __init__ are two seperated channels. Consult the diagram in Object Create Sequence. So the __init__ of Foo is executed no mattar what __new__ of Singleton returned. This make value reassigned.

The approach to avoid __init__ get called in second time construction of object is finding where the __init__ is executed and try to avoid it. Obviously, the logic of __init__ is embedded in __call__ of metaclass, type in this case, as shown in Object Create Sequence. So eventually, we need a new metaclass and a different logic of __call__ implementation. The new logic return the cached instance if there is an instance cached in _instances``and avoid calling ``__new__ and __init__ totally. This is exactly what we do in correct implementation in the beginning part of this section.