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.

Pyc. 26 Animal with a multiton decoration, classified per animal name
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.

Pyc. 27 Animal with a multiton decoration for one unique instance per name
# 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.

Pyc. 28 Animal with a multiton decoration for one unique instance per equivalence class ‘y’ in name
# 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:

Pyc. 29 Some alternative decoration examples
# 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:

Pyc. 30 An example sequence with seal, unseal, reset, get_instances, …
# 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:

Pyc. 31 Decoration as a multiton is thread-safe
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.


◄ 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.