Multiton¶
A multiton pattern is a design pattern that extends the singleton pattern. Whereas the singleton allows for exactly one instance per class, the multiton ensures one single (unique) instance per key.
In this implementation, the key parameter can be anything that is possible as
a key for a Python dict()
dictionary, such as an immutable type or a
callable eventually returning such an immutable type etc.
In case of an invalid key, key is set None
and with only
one key value the multiton simply collapses to a singleton, therefore the
decoration @Multiton
resp. @Multiton()
or even @Multiton(key=17)
or @Multiton(key='some constant value')
and so on always creates a
singleton.
A Multiton with a constant key collapses to a Singleton |
Normally, the key is part of or is composed from the initial values of the
classified object, as in the following example, where the key function matches
the signature of the initializer and uses the initial value of the name
parameter to construct a key value for the instances of Animal
.
from decoratory.multiton import Multiton
@Multiton(key=lambda spec, name: name)
class Animal:
def __init__(self, spec, name):
self.spec = spec
self.name = name
def __repr__(self):
return f"{self.__class__.__name__}('{self.spec}', '{self.name}')"
# Create Instances
a = Animal('dog', name='Teddy')
b = Animal('cat', name='Molly')
c = Animal('dog', name='Roxie')
When instances of the class Animal
are now created, this only happens for
the first instantiation per key value, the initial name of the animal. For
all subsequent instantiations, this primary instance per key value is
returned. But for each new key value, a new Animal
instance is created
and stored in the internal directory.
# Case 1: decoration @Multiton(key=lambda spec, name: name)
print(a) # Animal('dog', 'Teddy')
print(b) # Animal('cat', 'Molly')
print(c) # Animal('dog', 'Roxie')
With three different names, a separate instance is created in each case.
In contrast, the following variant distinguishes only two types (equivalence
classes): animals with a character ‘y’ in their name and those without and
thus the key values can only be True
(for Teddy) or False
(for Roxie)
and Molly is late and therefore not instantiated.
# Case 2: decoration @Multiton(key=lambda spec, name: 'y' in name)
print(a) # Animal('dog', 'Teddy')
print(b) # Animal('dog', 'Teddy')
print(c) # Animal('dog', 'Roxie')
The initial parameter values of the initializer can also be accessed by their
args
-index or kwargs
-name. So the following decorations are also
possible:
# Case 3: One unique instance per specie
@Multiton(key="{0}".format) # spec is args[0]
class Animal:
pass # Some code ...
# Case 4: One unique instance per name
@Multiton(key="{name}".format) # name is kwargs['name']
class Animal:
pass # Some code ...
# Case 5: One unique instance for all init values, i.e. no duplicates
@Multiton(key=lambda spec, name: (spec, name))
class Animal:
pass # Some code ...
# Case 6: One unique instance from a @staticmethod or @classmethod
@Multiton(key=F("my_key")) # Late binding with F(classmethod_string)
class Animal:
pass # Some code ...
@classmethod
def my_key(cls, spec, name):
return 'y' in name
To actively control access to new equivalence classes, Multiton
provides
the seal()
, unseal()
, and issealed()
methods for sealing, unsealing,
and checking the sealing state of the Multiton
. By default, the sealing
state is set False
, so for every new key a new (unique) object is
instantiated. When sealed (e.g. later in the process) is set True
the
dictionary has completed, i.e. restricted to the current object set and
any new key raises a KeyError
.
In situations where it might be useful to reset the multiton to express in
code that instances are often retrieved but rarely modified, setting the
decorator parameter resettable=True
will expose the reset()
method,
by means of which the internal directory of instances can be completely cleared.
Last but not least, Multiton
provides a instances
property and
associated getter and setter methods to directly retrieve the internal
dictionary of primary instances. It is obvious that manipulations on this
directory can corrupt the functionality of the multiton, but sometimes it
is useful to have the freedom of access.
Warning
Multiton — changes affecting key values of classified objects
Classifications into the multiton directory are done only once on initial key data. Subsequent changes affecting a key value are not reflected in the multiton directory key, i.e. the directory may then be corrupted by such modifications.
Therefore, never change key related values of classified objects!
All these things taken together could give the following exemplary picture:
# Case 7: with decoration @Multiton(key=lambda spec, name: name,
# resettable=True)
Animal.reset() # Possible, because of resettable=True
print(Animal.get_instances()) # {}
print(Animal.issealed()) # False (=default)
Animal('dog', name='Teddy') # Animal('dog', 'Teddy')
print(Animal.get_instances()) # {'Teddy': Animal('dog', 'Teddy')}
Animal.seal() # Seal the multiton!
print(Animal.issealed()) # True
try: # Try to..
Animal('cat', name='Molly') # .. add a new animal
except KeyError as ex: # .. will fail
print(f"Sorry {ex.args[1]}, {ex.args[0]}")
print(Animal.get_instances()) # {'Teddy': Animal('dog', 'Teddy')}
Animal.unseal() # Unseal the multiton!
print(Animal.issealed()) # False
Animal('cat', name='Molly') # Now, Molly is added
print(Animal.get_instances()) # {'Teddy': Animal('dog', 'Teddy'),
# 'Molly': Animal('cat', 'Molly')}
Animal.get_instances().pop('Teddy')
print(Animal.get_instances()) # {'Molly': Animal('cat', 'Molly')}
Animal.get_instances().clear() # Same as Animal.reset()
print(Animal.instances) # {}
The last two lines show functional equivalence of
Animal.get_instances().clear()
resp. Animal.instances.clear()
with Animal.reset()
, but the reset
option is more transparent because
it is not necessary to look “behind the scenes”.
Multithreading¶
Within the main process of Python’s Global Interpreter Lock (GIL), Multiton
is 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.multiton import Multiton
from concurrent.futures import ThreadPoolExecutor, as_completed
@Multiton(key=lambda spec, name: spec)
class Animal:
def __init__(self, spec, name):
self.spec = spec
self.name = name
def __repr__(self):
return f"{self.__class__.__name__}('{self.spec}', '{self.name}')"
# Create Instances
pets = [('dog', 'Teddy'), ('dog', 'Roxie'), # dogs
('cat', 'Molly'), ('cat', 'Felix')] # cats
with ThreadPoolExecutor(max_workers=2) as tpe:
futures = [tpe.submit(Animal, *pet) for pet in pets]
# Case 8: Decoration using spec: @Multiton(key=lambda spec, name: spec)
for future in futures: # Same instance per spec (=key), e.g.
instance = future.result() # Animal('dog', 'Teddy') - for all dogs
print(instance) # Animal('cat', 'Molly') - for all cats
Per type of animal (key = spec
) always the same unique instance is
presented, most likely Animal('Teddy')
for all dogs and
Animal('cat', 'Molly')
for all cats, resulting from the first submitted
thread per animal type, but it could also be any of the others.
© Copyright 2020-, Martin Abel, eVation. All Rights Reserved. |