Singleton

A singleton pattern is a design pattern that limits the instantiation of a class to a single (unique) instance. This is useful when exactly one unique object is needed i.e. to manage an expensive resource or coordinate actions across module boundaries.

As a simple example serves the decoration of the class Animal as a singleton. In the context of the Decorator Arguments Template as shown in Pyc. 14, this can be done both without brackets (decorator class) and with brackets (decorator instance), meaning both notations describe the same functional situation.

Pyc. 20 Animal with a Singleton resp. Singleton() decoration
from decoratory.singleton import Singleton

@Singleton                      # or @Singleton()
class Animal:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"{self.__class__.__name__}('{self.name}')"

# Create Instances
a = Animal(name='Teddy')        # Creates Teddy, the primary instance
b = Animal(name='Roxie')        # Returns Teddy, no Roxie is created

If instances of the class Animal are now created, this is only done for the very first instantiation, and for all further instantiations always this primary instance is given back.

Pyc. 21 Verfication of the unique singleton instance
# Case 1: Static decoration using @Singleton or @Singleton()
print(f"a = {a}")               # a = Animal('Teddy')
print(f"b = {b}")               # b = Animal('Teddy')
print(f"a is b: {a is b}")      # a is b: True
print(f"a == b: {a == b}")      # a == b: True

If instead of the above static decoration as in Pyc. 2 using pie-notation, i.e. with @-notation at the class declaration, the dynamic decoration as in Pyc. 1 within Python code is used, additional parameters can be passed to the decorator for passing to or through the class initializer.

Pyc. 22 Dynamic singleton decoration with extra parameters
# Case 2: Dynamic decoration providing extra initial default values
Animal = Singleton(Animal, 'Teddy')
Animal()                        # Using the decorator's default 'Teddy'
a = Animal(name='Roxie')        # Returns Teddy
print(a)                        # Animal('Teddy')

Quite generally, for all decorators based on the Decorator Arguments Template as shown in Pyc. 14, these two properties are always fulfilled:

Conclusions from the Decorator Unification Protocol:

  1. For dynamic decoration, extra parameters can be passed, e.g. for the class initializer (1. Requirement)

  2. Decoration as a class (without brackets) and Decoration as an instance (with empty brackets) are equivalent (3. Requirement)

Semi-Singleton Extension

So far, this singleton implementation follows the concept of once forever, i.e. whenever a new instance of a class is created, one always gets the primary instance back - without any possibility of ever changing it again.

Although this behavior is consistent with the fundamental concept of a singleton, there are situations where it might be useful to reset a singleton. Such a resettable singleton, also called semi-singleton, could be useful to express in code that an instance is often retrieved but rarely changed.

Pyc. 23 Decoration as a resettable singleton
from decoratory.singleton import Singleton

@Singleton(resettable=True)     # Exposes an additional reset method
class Animal:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"{self.__class__.__name__}('{self.name}')"

# Case 3: Decoration using @Singleton(resettable=True)
print(Animal(name='Teddy'))     # Animal('Teddy')
print(Animal(name='Roxie'))     # Animal('Teddy')   (=primary instance)
Animal.reset()                  # Reset the singleton
print(Animal(name='Roxie'))     # Animal('Roxie')
print(Animal(name='Teddy'))     # Animal('Roxie')   (=primary instance)

Without this striking resettable=True decoration Animal has no reset method and the call Animal.reset() will fail raising an AttributeError. For situations where this concept needs to be used more often, a subclass shortcut SemiSingleton is provided.

Pyc. 24 Decoration as a semi-singleton
from decoratory.singleton import SemiSingleton

@SemiSingleton                  # or @SemiSingleton()
class Animal:
    pass                        # Some code ...

get_instance()

Both Singleton and SemiSingleton of course provide a get_instance() method to directly retrieve the primary instance, e.g. using Animal.get_instance(). The use of get_instance() makes it clear in the code that the instance of a singleton is requested, which is good style in principle. However, when using a SemiSingleton:

Warning

SemiSingleton — using combined reset() and get_instance()

It should be noted that the combination of reset() and immediately following get_instance() does not return a valid object, but None. So a reset() should always be followed by an instantiation to ensure that a valid SemiSingleton instance exists.

Multithreading

Within the main process of Python’s Global Interpreter Lock (GIL), both Singleton and SemiSingleton are thread-safe. In example, using a ThreadPoolExecutor from concurrent.futures, providing a high-level interface for asynchronously executing callables, threadsafety can be easily demonstrated with sample code like this:

Pyc. 25 Decoration as a singleton is thread-safe
from decoratory.singleton import Singleton
from concurrent.futures import ThreadPoolExecutor, as_completed

@Singleton                      # or @Singleton()
class Animal:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"{self.__class__.__name__}('{self.name}')"

# Create Instances
names = ["Teddy", "Roxie", "Molly", "Felix"]
with ThreadPoolExecutor(max_workers=2) as tpe:
    futures = [tpe.submit(Animal, name) for name in names]

# Case 5: Decoration using thread-safe @Singleton
for future in futures:
    instance = future.result()  # All the same instances, e.g.
    print(instance)             # Animal('Teddy') -- four times!

A single unique instance is always presented, most likely Animal('Teddy') of the first submitted thread, but it could also be any of the others.


◄ prev

up

next ►

Legal Notice

Privacy Policy

Cookie Consent

Sphinx 7.2.6 & Alabaster 0.7.12

© Copyright 2020-, Martin Abel, eVation. All Rights Reserved.