Singleton is a creational design pattern, which ensures that only one object of its kind exists and provides a single point of access to it for any other code.
Singleton has almost the same pros and cons as global variables. Although they’re super-handy, they break the modularity of your code.
You can’t just use a class that depends on a Singleton in some other context, without carrying over the Singleton to the other context. Most of the time, this limitation comes up during the creation of unit tests.
Usage examples: A lot of developers consider the Singleton pattern an antipattern. That’s why its usage is on the decline in Python code.
Identification: Singleton can be recognized by a static creation method, which returns the same cached object.
Naïve Singleton
It’s pretty easy to implement a sloppy Singleton. You just need to hide the constructor and implement a static creation method.
The same class behaves incorrectly in a multithreaded environment. Multiple threads can call the creation method simultaneously and get several instances of Singleton class.
main.py: Conceptual example
class SingletonMeta(type): """ The Singleton class can be implemented in different ways in Python. Some possible methods include: base class, decorator, metaclass. We will use the metaclass because it is best suited for this purpose. """ _instances = {} def __call__(cls, *args, **kwargs): """ Possible changes to the value of the `__init__` argument do not affect the returned instance. """ if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] class Singleton(metaclass=SingletonMeta): def some_business_logic(self): """ Finally, any singleton should define some business logic, which can be executed on its instance. """ # ... if __name__ == "__main__": # The client code. s1 = Singleton() s2 = Singleton() if id(s1) == id(s2): print("Singleton works, both variables contain the same instance.") else: print("Singleton failed, variables contain different instances.")
Output.txt: Execution result
Singleton works, both variables contain the same instance.
Thread-safe Singleton
To fix the problem, you have to synchronize threads during the first creation of the Singleton object.
main.py: Conceptual example
from threading import Lock, Thread class SingletonMeta(type): """ This is a thread-safe implementation of Singleton. """ _instances = {} _lock: Lock = Lock() """ We now have a lock object that will be used to synchronize threads during first access to the Singleton. """ def __call__(cls, *args, **kwargs): """ Possible changes to the value of the `__init__` argument do not affect the returned instance. """ # Now, imagine that the program has just been launched. Since there's no # Singleton instance yet, multiple threads can simultaneously pass the # previous conditional and reach this point almost at the same time. The # first of them will acquire lock and will proceed further, while the # rest will wait here. with cls._lock: # The first thread to acquire the lock, reaches this conditional, # goes inside and creates the Singleton instance. Once it leaves the # lock block, a thread that might have been waiting for the lock # release may then enter this section. But since the Singleton field # is already initialized, the thread won't create a new object. if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] class Singleton(metaclass=SingletonMeta): value: str = None """ We'll use this property to prove that our Singleton really works. """ def __init__(self, value: str) -> None: self.value = value def some_business_logic(self): """ Finally, any singleton should define some business logic, which can be executed on its instance. """ def test_singleton(value: str) -> None: singleton = Singleton(value) print(singleton.value) if __name__ == "__main__": # The client code. print("If you see the same value, then singleton was reused (yay!)\n" "If you see different values, " "then 2 singletons were created (booo!!)\n\n" "RESULT:\n") process1 = Thread(target=test_singleton, args=("FOO",)) process2 = Thread(target=test_singleton, args=("BAR",)) process1.start() process2.start()
Output.txt: Execution result
If you see the same value, then singleton was reused (yay!) If you see different values, then 2 singletons were created (booo!!) RESULT: FOO FOO