Reputation: 4534
It's not clear to me how the typical metaclass singleton implementation works. I would expect the starred print to execute twice; it only happens once:
class _Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
print('Within __call__', cls, cls._instances, *args, **kwargs)
if cls not in cls._instances:
print('**About to call __call__', super(_Singleton, cls).__call__, flush=True)
print("Is cls the '...of object'?", hex(id(cls)).upper())
cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)
print(cls._instances[cls])
return cls._instances[cls]
class MySingleton(metaclass=_Singleton):
pass
if __name__ == '__main__':
print('Making mysingleton')
mysingleton = MySingleton()
print("Is mysingleton the 'cls'?", mysingleton)
print('Making another')
another = MySingleton()
# Verify singletude
assert another == mysingleton
This prints
Making mysingleton
Within __call__ <class '__main__.MySingleton'> {}
**About to call __call__ <method-wrapper '__call__' of _Singleton object at 0x000001C950C28780>
Is cls the '...of object'? 0X1C950C28780
<__main__.MySingleton object at 0x000001C9513FCA30>
Is mysingleton the 'cls'? <__main__.MySingleton object at 0x000001C9513FCA30>
Making another
Within __call__ <class '__main__.MySingleton'> {<class '__main__.MySingleton'>: <__main__.MySingleton object at 0x000001C9513FCA30>}
As is my experience with the Python docs, they're terribly circular and confusing. The docs say that __call__()
is called when an instance is "called". Fair enough; MySingleton.__call__
is run because the "()" on mysingleton = MySingleton()
indicates a function call. MySingleton is of the _Singleton type, so it has an _instances dict. The dict is naturally empty on first call. The conditional fails and Super(_Singleton, cls).__call__
executes.
What does super() do here? It's barely intelligible from the docs. It returns a "proxy object", explained elsewhere as "an object which 'refers' to a shared object", that delegates method calls to a parent or sibling class of 'type'. Okay, fine; it will be used to call a method of some related _Singleton type.
The two argument form of super(), which is used here, "specifies the arguments exactly and makes the appropriate references". What references are those? The type is _Singleton
and object is cls, which isn't mysingleton
. It's whatever object 0x000001C950C28780
is. Anyway, the super() search order is that of getattr()
or super()
. I think that means references are looked up according to _Singleton.__mro__
since __call__
isn't an attribute (or is it?). That is, the super() call looks up according to super(), which I assume is _Singleton. Clear as mud. The __mro__
yields (<class '__main__._Singleton'>, <class 'type'>, <class 'object'>)
. So, super(_Singleton, cls)
will look for the "related _Singleton type" and call its __call__
method; I assume that's cls.__call__()
.
Since cls is a _Singleton, I would expect to see the second print. Actually, I would expect some kind of recursion. Neither happen. What's going on in there?
Upvotes: 0
Views: 1130
Reputation: 148975
The super
builtin is not the most simple thing in Python syntax. It is used when a method has been overriden in a hierarchy of classes and allows to specify indirectly which version (the method defined in which ancestor class) will actually be called.
Here, _Singleton
is a subclass of type
. Fair enough. But as the __call__
method of _Singleton
has been overriden, it will have to call the same method in its parent class to actually build an object. That is the purpose of super(_Singleton, cls).__call__(*args, **kwargs)
: it will forward the call to the parent of _Singleton
. So it is the same as:
type.__call__(cls, *args, **kwargs)
That is: it call the __call__
method of type
but still uses cls
as a self
object allowing the creation a MySingleton
object and bypassing a recursive call to _Singleton.__call__
. Alternatives would be to use S.__new__(S, *args, **kwargs)
or directly object.__new__(S)
, but that last one would bypass any possible object initialization.
In fact, super
is the Pythonic way here, because if you later build a more complex hierarchy of metaclasses (_Singleton <- _GenericMeta <- type), super(_Singleton, cls)
will ensure to use the class immediately preceding _Singleton
in the hierachy.
Upvotes: 1