Notes and exercises for learning design patterns
A regular class uses type as its metaclass.
So this:
RegularSettings("development")
roughly goes through:
type.__call__(RegularSettings, "development")
That normal call path creates a new object every time.
SingletonMetaWhen we write:
class Settings(metaclass=SingletonMeta):
...
then this:
Settings("development")
runs through:
SingletonMeta.__call__(Settings, "development")
That is the one doorway we changed.
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
super().__call__(*args, **kwargs) means:
Use the normal class-call behavior to create and initialize the object.
But we only do that once per class.
The cache is here:
SingletonMeta._instances
The keys are classes:
Settings
PluginRegistry
The values are the one cached instance for each class.
So Settings and PluginRegistry do not share the same object. Each class gets one object of its own.
__init__ runs only once__init__ runs inside super().__call__.
Since the metaclass calls super().__call__ only when the class is missing from the cache, initialization also happens only once.
That is different from the basic __new__ version, where you often need an explicit _initialized guard.
The decorator version replaces the class name with a function.
The metaclass version keeps Settings as a real class.
That is why this still works:
isinstance(settings, Settings)
Metaclasses are powerful but less familiar. Use this approach when preserving class identity matters or when several classes should share the same creation rule.
For a small one-off singleton, a module-level object or a simpler implementation may be easier to read.