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.
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.
# 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.
# 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:
|
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.
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.
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:
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.
© Copyright 2020-, Martin Abel, eVation. All Rights Reserved. |