So recently I implemented the singleton pattern in python and since I come from other languages, I did it as below
class SingletonOrdinary:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
self.myMember = 42
print("Inside init for SingletonOrdinary")
if __name__ == "__main__":
singleton1 = SingletonOrdinary()
print(singleton1.myMember) # Output: 42
singleton2 = SingletonOrdinary()
singleton1.myMember = 99
assert(singleton2.myMember == 99)
assert(singleton1 is singleton2) # Output: True, both are the same instance
So the usual. In the ctor we check for if an instance of the class already exists and if so return that. Bam. I wiped my brow, hit enter and what do I see? "Inside init for SingletonOrdinary" prints twice. Huh? Well thats not good. Because if there's some heavy lifting initialization occurring inside __init__() I certainly dont want that occurring everytime but just once when the singleton instance is created.
So like any good programmer, I hit up chatGPT and asked them "So why does __init__() run twice? Isnt __new__() returning the same instance? I thought __init__() runs only when a new instance is created but as per my original code (ie, without the guard in __init__()) the instance is created just once no?"
Here's their answer:
You're absolutely right: in your singleton setup, __new__() only creates the instance once. Subsequent calls return the same instance. That’s working exactly as expected.
BUT… here's the twist:
🔥 Python still calls __init__() on the returned object every time you call the class — even if the instance already exists.
So thats just my lack of knowledge about python. TIL.
So we fix it using a guard in __init__() as below:
class SingletonOrdinary:
_instance = None
_initialized = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if self._initialized: # this is the guard
return
self.myMember = 42
print("Inside init for SingletonOrdinary")
self._initialized = True
And then I thought I was done. Wrong. I took a peek at my colleagues singleton implementation and it was as below:
class SingletonMeta(type):
"""
A metaclass for creating singleton classes.
"""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class MySingleton(metaclass=SingletonMeta):
def __init__(self):
self.myMember = 42
print("Inside init for MySingleton")
if __name__ == "__main__":
singleton1 = MySingleton()
print(singleton1.myMember) # Output: 42
singleton2 = MySingleton()
singleton1.myMember = 99
assert(singleton2.myMember == 99)
assert(singleton1 is singleton2) # Output: True, both are the same instance
(*゚ロ゚)
Metaclass, __call__() what? And it worked. Tch. Showoff. Get a life bro. Nerd.
Sigh... Nah, this is just better. Its reusable. All I need to create a new singleton is use SingletonMeta as a metaclass.
What even is a metaclass?
Basically its a form of metaprogramming that python supports. And that its not generally a good idea to use. Anticlimatic. Just read this for more info.
Ok, sounds good. Then I tried to use the same class used in metaclass but via inheritance
class SingletonRoot():
_instances = {}
def __call__(cls, *args, **kwargs):
print("Inside SingletonRoot __call__")
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class MySingleton(SingletonRoot):
def __init__(self):
self.myMember = 42
print("Inside init for MySingleton")
if __name__ == "__main__":
singleton1 = MySingleton()
print(singleton1.myMember) # Output: 42
singleton2 = MySingleton()
singleton1.myMember = 99
assert(singleton2.myMember == 99)
assert(singleton1 is singleton2) # Output: True, both are the same instance
and this does not work. What doesnt work and why?
assert(singleton2.myMember == 99) fails. Which means that the whole singleton thing itself is not working. And this is because __call__() is not getting invoked when I do MySingleton(). MONEY!!! And why is that?
When we do singleton = MySingleton() python essentially translates it to singleton = type(MySingleton).__call__(MySingleton, *args, **kwargs)
See the type(MySingleton) above? And thats why it works with metaclass and __call__().
Python does not invoke MySingleton.__call__(). Which would happen if we did
instance = MySingleton()
instance() # here MySingleton.__call__() is invoked
Just as a side note, I tried to use inheritance with __new__(). Forget metaclass and __call__() and its a pain to get it working correctly. I do think its possible to get it working but I just dont think its proper to implement singleton using inheritance.
Whats the difference between __new__() and __call__()? Well, they're both magic method in python but are invoked in different phases of an object creation.
When we do instance = MySingleton() the __new__() is invoked which creates the class object (ie, MySingleton and not the instance of it yet. Thats done by __init__()). Also this is when the __call__() is invoked as instance = type(MySingleton).__call__(MySingleton, *args, **kwargs). Im not sure if __call__() invokes __new__() or vice versa or not.
More tangent: I tried to get a hacky version of singleton working with metaclass and __new__() instead of __call__() but it does NOT work. It can be made to work but its just too confusing, unnecessary and not good and makes me want to end it all.
In minecraft ofcourse.
Finally we dont have to use a dict in __call__() but its the right way. I dont fully understand but if we dont then it works but accidentally. So using a dict is the right way when we intend to use the metaclass to create singleton versions of different classes.
Big post but yea. Fun stuff.