Singleton in python

2019-02-24
Python



Python classes are very useful but sometimes they lack some features that might be very useful.

Table of Contents

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()
Out: 1
loader2.get_value()
Out: 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()
Out: 1
single_loader2.get_value()
Out: 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 here.

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()
Out: 1
loader2.get_value()
Out: 1

3.2. 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()}")
Out: 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()}")
Out: 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()}")
Out: Loader_1: 2. Loader_2: 7

More info about classes variables in this video.