Singleton Pattern in Python
Do not miss this exclusive book on Binary Tree Problems. Get it now for free.
In this article, we have explored the concept of Singleton Design Pattern in Python. Singleton Pattern is one of the most common design patterns.
Objectives
- Define Design Patterns and list the types.
- Define Singleton Pattern
- Implement Singleton Pattern and apply it in various applications and systems using Python and Similar OOP Languages.
- Write reusable code using Singleton Pattern.
What are Design Patterns
Design patterns are reusable formatted best practices that programmers can use to solve common problems when designing applications or systems. They can be classified into Creational Patterns Structural Patterns, Behavioural Patterns, and Concurrency Patterns.
Singleton Pattern
It is a type of creational pattern dealing with object creation. The pattern restricts the instantiation of a class to one single instance. It is used when only one object is needed to coordinate actions across applications. Singleton pattern is useful in logging, driver objects, caching, and thread pool. Python module made use of singleton pattern. Python checks whether a module has been imported, if imported it returns the object for the module, and if not it creates it.
UML Diagram of Singleton class
The ideology behind the creation of the singleton class is as follows:
- We will allow a singleton class instance to be created on the first attempt.
- If an instance already exists, we will return the previously created instance of the singleton class.
Implementing this can be done in __ new __
python magic method. The code is as follows:
class Singleton:
def __new__(cls, *args):
if not hasattr(cls, 'instance'):
cls.instance = super(Singleton, cls).__new__(cls)
return cls.instance
s = Singleton() # newly created instance
s1 = Singleton() # returned the previously created instance - s
print(f"{s=}\n{s1}")
In the preceding snippet __ new __
magic method was overridden to control objects creation. s
object got created with the __ new __
method while s1
returns the previously created object instance. The logic behind the snippet is that hasattr
builtin function checks if the object class to be created has an instance attribute, if it doesn't, a new object will be created, and if it does have instance attribute, the previously created object will be returned. You'll also notice that the id
of both objects are the same. The results are shown below:
s=<__main__.Singleton object at 0x7fe9df1986d0>
s1=<__main__.Singleton object at 0x7fe9df1986d0>
The above implementation is the Gang of Four(GoF) implementation of the Singleton pattern. There is also another way we can declare a Singleton class. The idea behind this is that objects can be created as many times as we want but they should all share the same states and behaviors. This is what we call the Borg or Monostate pattern.
Python uses __ dict __
to store the state of every object of a class and that is what we will manipulate to share the state across all objects of a class. Below is the implementation.
class Borg:
__shared_state = {'borg': 'Monostate'}
def __init__(self):
self.x = 'Design Pattern'
self.__dict__ = self.__shared_state
pass
b = Borg()
b1 = Borg()
b.x = 2100
print(f"b {b.__dict__}, {b.x=}")
print(f"b1 {b1.__dict__}, {b1.x=}")
The results of the preceding snippets are given below:
b {'borg': 'Monostate', 'x': 2100}, b.x=2100
b1 {'borg': 'Monostate', 'x': 2100}, b1.x=2100
Singleton and metaclasses
A metaclass is a class of class. It is through metaclass that programmers are able to create their classes from python predefined classes. As a matter of fact, everything in python is an object.
As metaclass has more control over class creation and object instantiation, it can be used to create Singletons. Metaclasses in python override the __ new __
and __ init __
or __ call __
magic methods. Below is the sample code for Singleton implementation with metaclasses.
class MetaSingleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Logger(metaclass=MetaSingleton):
pass
log1 = Logger()
log2 = Logger()
print(f"{log1=}\n{log2=}")
The results of the above snippets are shown below:
log1=<__main__.Logger object at 0x7f5e89a79450>
log2=<__main__.Logger object at 0x7f5e89a79450>
As excepted the logger instances are the same object(same id i.e pointing to the same object).
Real World example of Singleton pattern
We will now consider an example of a cloud service that involves multiple read and write operations on the database. What we need to singleton here is the database because the web app will call an API which eventually operates on the database. We will make use of sqlite3, it comes bundled with python. The code is shown below.
import sqlite3
class MetaSingleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=MetaSingleton):
connection = None
def connect(self):
if self.connection is None:
self.connection = sqlite3.connect('db.sqlite3')
self.cursorobj = self.connection.cursor()
return self.cursorobj
db1 = Database().connect()
db2 = Database().connect()
print(f"{db1=}\n{db2=}")
The results are as follows:
db1=<sqlite3.Cursor object at 0x7f5e88502340>
db2=<sqlite3.Cursor object at 0x7f5e88502340>
From the above, you can see that the cursorobj
is the same for all objects created from the Database
class, including the Database
class(Gof Singleton).
Drawbacks of Singleton pattern
- Global variables can be changed by mistake.
- Multiple references may get created to the same object
- Classes are tightly coupled, so changing one class inadvertently impacts the other class
Sign up for FREE 3 months of Amazon Music. YOU MUST NOT MISS.