Singleton in python
0. Intro
Python classes are very useful but sometimes they lack some features that might be very useful.
1. The problem
Let’s imagine you want to create a class that have some data. This class will also have a sync function to update the data. As an example let’s create two loader instances of the same class.
class loader:
def __init__(self):
self.value = 0
def sync(self):
""" This will only sync the current instance """
self.value += 1
def get_value(self):
return self.value
loader1 = loader()
loader2 = loader()
After synchronizing the data loader1
will have value = 1
. But loader2
will have value = 0
loader1.sync()
loader1.get_value()
> 1
loader2.get_value()
> 0
If what you need is to use this data in multiple python files and you want to only call the sync function once and not in every python file this won’t work as expected.
2. The singleton pattern
What is needed is a class that can only have one instance. This is the singleton (more info at Python3 patterns and idioms). The simpliest way to achive that is by creating a new python file since by design it will be unique.
So for example let’s create single_loader.py
.
data = {"value": 0}
def sync():
# It is important to update the data variable instead of assign a new value
# data = {"value": data.get("value", 0) + 1} won't work as expected
data.update({"value": data.get("value", 0) + 1})
def get_value():
return data.get("value", 0)
This file will also have the sync
and get_value
functions.
Now we can call the single_loader.py
in any file.
import single_loader
single_loader1 = single_loader
single_loader2 = single_loader
This time if we do the same as the example above the get_value
function will always return the same value.
single_loader1.sync()
single_loader1.get_value()
> 1
single_loader2.get_value()
> 1
2.1. On real example
While I was working with one of my Dash projects I made use of that to create a data_loader.py
file. You can view the code Expensor personal.
What I have is a data_loader.py
that implements a sync
function to update both DFS
and YML
vars. Those two vars are then used in all files inside the pages
folder. And the sync
function of the data_loader.py
is only called in index.py/update_sync_count()
.
3. Another option: Using class variables
As pointed out by Gustau in the comments there is another way to do it using classes.
class single_loader2:
value = 0
@staticmethod
def sync_all():
""" This will update the value in ALL instances"""
single_loader2.value += 1
def get_value(self):
return self.value
loader1 = single_loader2()
loader2 = single_loader2()
This time we will call directly the class variable and the result will be the same as the example above.
loader.sync_all() # It is a staticmethod, you can call directly the class function
loader1.get_value()
> 1
loader2.get_value()
> 1
3.1. Mixing the two methods
Be careful, you probably don’t want to mix both types of variable assignments because it can yield unexpected results. Let’s take the following example:
class mloader:
value = 0
def sync(self, new_value):
""" This will only sync the current instance. If you use that one time the sync all won't work"""
self.value = new_value
@staticmethod
def sync_all(new_value):
""" This will update the value in ALL instances"""
mloader.value = new_value
def get_value(self):
return self.value
mloader_1 = mloader()
mloader_2 = mloader()
You can then call the staticmethod sync_all
and all will work as expected
mloader.sync_all(5)
print(f"Loader_1: {mloader_1.get_value()}. Loader_2: {mloader_2.get_value()}")
Loader_1: 5. Loader_2: 5
And of course you can call the sync
function of one of the loaders:
mloader_1.sync(2)
print(f"Loader_1: {mloader_1.get_value()}. Loader_2: {mloader_2.get_value()}")
Loader_1: 2. Loader_2: 5
But after that the sync_all
won’t sync both instances:
mloader.sync_all(7)
print(f"Loader_1: {mloader_1.get_value()}. Loader_2: {mloader_2.get_value()}")
Loader_1: 2. Loader_2: 7
More info about classes variables in this video from Corey Schafer: