Using Python types for fun and profit: the Singleton

Introduction

The singleton pattern restricts the instantiation of a class to a single instance. The singleton pattern makes it possible to ensure that:

  1. The creation of a class is controlled (in some languages like C# or Java this is done by making the constructors private)
  2. The one instance we have is easily accessible
  3. The singleton pattern ensures we only have at most one instance of a class during the running of our application.

One of the main uses of the singleton pattern is logging, as all clients who wish to log need a single point of entrance.

The UML diagram of this pattern is very simple:

A Singleton has

  • a private constructor, or a static constructor. In the constructor, a check is done to see if there is an instance of the Singleton. If there is, that instance is returned, if not a new one is made, stored and returned
  • It contains an instance of itself, in some languages this is stored in a private field.
  • The getInstance() method is used to get the single instance. If there is no instance yet, the constructor is called and a new instance is made.

The use of singletons is usually not recommended, and even though they are one of the seemingly simpler Design Patterns, their implementations can lead to some difficulties especially in multithreaded environments. Therefore we will build a threadsafe implementation in Python. In our example we will make our Singleton threadsafe, with the use of locking.

In this article I will be using Python 3.12, so the code might not work on earlier versions

Implementing a threadsafe Singleton in Python with type-annotations

We will start with the preliminaries:

import threading
import random
import time

Next we implement the Singleton:

class Singleton:
    _instance_lock: threading.Lock = threading.Lock()
    _instance = None
    _value: int = None

    def __init__(self):
        pass

    def __new__(cls):
        if cls._instance is None:
            with cls._instance_lock:
                if cls._instance is None:
                    cls._instance = super(Singleton, cls).__new__(cls)
                    cls._instance.init_singleton()
        return cls._instance

    def init_singleton(self):
        self._value = 0

    def increment_value(self):
        self._value += 1
        return self._value

This code certainly needs some explanation:

  • We will need a lock, hence we need a lock object. We use the Lock object for that.
  • We also need to have a somewhere to store our singleton object, that is what the _instance variable is for
  • We really do not need to do anything in the constructor, so we simply implement that with a pass statement.
  • Because the constructor is empty, we define the _value field to be none, right next to all the other instance variables.

Note the type-annotations, and the one lacking for the _instance variable. Apparently in Python you can not have an instance variable of the same class as the enclosing class. This is not a big problem here because:

  • The _instance variable is private
  • The intent of the variable through the use of a descriptive name is clear.

The __new__() method

So what happens:

  1. We check whether we already have an instance, if we have one, then return that. This is no different from non-threadsafe implementations of this pattern
  2. If there is none, we try and lock it, by using the ‘with cls._instance_lock’. This statement does three things: 1. It waits till the lock is release if it is locked. 2. Once it acquires the lock, it locks it. 3. After executing the block, the locks gets released. The code may seem cryptic, but what it basically is, is a block of code protected by a lock
  3. Because the _instance could have been set by another thread, we need to check it again. If there still is no instance, we construct it.
  4. The super(Singleton,cls) returns a temporary object which represents the parent of this class, i.e. object. The __new(cls)__class on this temporary object constructs an object of the Singleton class itself
  5. After that, the init_singleton() method is called, to initialize the instance. Because this is part of the locked block (it is all within the with statement), this happens only once.
  6. The use of the with statement, the lock gets unlocked automatically, so other threads can access the singleton
  7. After all this has been done we need to return the instance. That is one of the difference between the __new__()and the __init__() methods: __new__() has a return value, i.e. an object of the class, and __init__() has no return value, and is simply used for what is constructor logic. The other big difference is that __new__() is a static method, __init__() is an instance method.

The init_singleton() method

The method does nothing more that initialize our _value to zero.

The increment_value() method

This method simple increments the _value by one and returns it.

Putting it to the test

Now we can test it:

def run_thread():
s: Singleton = Singleton()
wait_time: int = random.randint(1, 4)
print(f"Thread {threading.current_thread().name} will wait {wait_time} seconds")
time.sleep(wait_time)
print(f"Thread {threading.current_thread().name} gets value {s.increment_value()}")


if __name__ == "__main__":
threads: list[threading.Thread] = []

for _ in range(10):
thread: threading.Thread = threading.Thread(target=run_thread)
threads.append(thread)
thread.start()

for thread in threads:
thread.join()

The run_thread() method

This method does the following:

  1. It acquires a Singleton object
  2. It generates a random number, and waits for that number seconds.
  3. Next it prints out the name of the current thread, and the current value.

The main test

Also here a line by line explanation is needed. Information on the threading library can be found here.

  1. A list of threads is initialized to an empty list. Observe that we explicitly define the type to be a list of threads
  2. Next we iterate 10 times, each time creating a new thread. The target parameter is a callable object, in our case a function.
  3. We append this thread to our thread list
  4. We start the thread
  5. Next we iterate over our threadlist, and we wait for all the threads to terminate

Try running this, and you should a random execution of threads while the value keeps steadily increasing

Conclusion

Once you understand the Python threading library, implementing a threadsafe Singleton is not too difficult. The ‘with’ statement to acquire and release locks, turns out to be quite powerful.

Even though the code is multithreaded, the flow remains clear. Adding some type-annotations makes the code easier to understand. However, clear naming can also be a great help with that.

Leave a Reply

Your email address will not be published. Required fields are marked *