3

I was toying around with the Singleton pattern and derivation. Specifically, I had this code:

class Singleton:
    _instance = None
    init_attempts = 0

    def __init__(self):
        self.init_attempts += 1
    
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super().__new__(cls, *args, **kwargs)
        return cls._instance


class Derived(Singleton):
    def __init__(self):
        super().__init__()
        self.attribute = "this is derived"


def main():
    instance_1 = Singleton()
    instance_2 = Singleton()

    print("instance_1 and instance_2 are ", end="")
    if id(instance_1) == id(instance_2):
        print("the ame")
    else:
        print("different")
    
    derived_instance = Derived()
    print("derived_instance and instance_2 are the same:", derived_instance is instance_2)
    
    try:
        print(derived_instance.attribute)
    except AttributeError:
        print("Initialization of Derived has been prevented")
    
    print("Number of initializations:", derived_instance.init_attempts)


if __name__ == '__main__':
    main()

To my surprise, the __init__ of Derived is never called, i.e. derived_instance.attribute does not exist and derived_instance.init_attempts remains 2. As I understand it, SomeClass() is resolved to SomeClass.__call__(), which in turn should call the __new__ and __init__ methods. Yet, in the above example, Derived.__init__ is never called.

In detail, my output is:

instance_1 and instance_2 are the ame
derived_instance and instance_2 are the same: True
Initialization of Derived has been prevented
Number of initializations: 2

Can anybody explain to me, why that is? In particular, the similar example here: How to initialize Singleton-derived object once works as expected, i.e. Tracer triggers printing init twice.

I am aware that deriving from Singletons cannot be done in a meaningful way, and that both, a Singleton decorator and metaclass exist and how to use them, so I'm specifically interested in how the inheritance process goes awry. I presume it has something to do with the MRO, but I cannot think of anything that would not also compromise the Tracer example from the link.

I tried finding similar links, but could not find any additional information.

This here asks a very similar question: does __init__ get called multiple times with this implementation of Singleton? (Python) but doesn't answer it, or rather only confirms that a derived class' ´__init__´ should get called as well.

Thanks in advance! Hoping to pay back the favour soon!

1 Answers1

2

The Python documentation has the answer:

If __new__() is invoked during object construction and it returns an instance of cls, then the new instance’s __init__() method will be invoked like __init__(self[, ...]), where self is the new instance and the remaining arguments are the same as were passed to the object constructor.

If __new__() does not return an instance of cls, then the new instance’s __init__() method will not be invoked.

In the case at hand, when Derived() (which has no __new__ of its own) is called to create a class instance, Singleton.__new__ is invoked, and because that does not return an instance of Derived (but of Singleton), the __init__() method will not be invoked.

Armali
  • 18,255
  • 14
  • 57
  • 171
  • The fact that `Derived` doesn't have its own `__new__` isn't all that relevant. The "problem" is that even if `cls` is `Derived`, `cls._instance` will see `Singleton._instance` (because inheritance), and thus no new instance of `Derived` will be created. – deceze Jun 28 '23 at 06:55
  • That depends; if `Derived` had its own `__new__` which would `return object.__new__(cls, *args, **kwargs)`, an instance of `Derived` would be created and `Derived.__init__()` would be called as well. – Armali Jun 28 '23 at 07:09
  • But then it won't be a singleton, because there will be different instances for `Singleton` and `Derived`. – Barmar Jun 29 '23 at 17:25