0

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?

Lorem Ipsum
  • 2,859
  • 3
  • 20
  • 50

1 Answers1

1

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.

Serge Ballesta
  • 121,548
  • 10
  • 94
  • 199
  • That clarifies what `super()` resolves to and how it avoids recursion. So then what does `type.__call__` actually do? The manual says "There are no special operations on types." I see that `type.__call__(cls, *args, **kwargs)` returns `<__main__.mysingleton at="" object="">`. I notice this is the same result as `type(cls.__name__, (), {})`. Does type just make a new type with the name of whatever object is passed to it? – Lorem Ipsum May 13 '21 at 18:45
  • I see that `type.__call__` is a ``. That looks like a wrapper for whatever implementation is written in C. – Lorem Ipsum May 13 '21 at 19:00
  • 1
    @LoremIpsum: Beware. `type(cls.__name__, (), {})` builds a class while `type.__call__(cls)` builds an instance of `cls`. – Serge Ballesta May 14 '21 at 08:49