floflo29
floflo29

Reputation: 2321

super in a metaclass

Recently, I used a metaclass to implement a singleton. Then, I tried to understand how this __metaclass__ stuff works. To do so, I wrote this piece of code:

import numpy as np

class Meta(type):
    _instance = None
    def __init__(cls, *args, **kwargs):
        print('meta: init')
        #return super(Meta, cls).__init__(*args, **kwargs)

    def __new__(cls, *args, **kwargs):
        print('meta: new')
        return super(Meta, cls).__new__(cls, *args, **kwargs)

    def __call__(cls, *args, **kwargs):
        print('meta: call')
        if cls._instance is None:
            cls._instance = super(Meta, cls).__call__(*args, **kwargs)
        print(cls._instance.__class__)
        return cls._instance



class ClassA():

    __metaclass__ = Meta
    def __init__(self, *args, **kwargs):
        self.val = np.random.randint(1000)
        print('classA: init')


    def __new__(cls, *args, **kwargs):
        print('classA: new')
        return super(ClassA, cls).__new__(cls, *args, **kwargs)

    def __call__(cls, *args, **kwargs):
        print('classA: call')
        return super(ClassA, cls).__call__(*args, **kwargs)

class ClassB():

    __metaclass__ = Meta
    def __init__(self, *args, **kwargs):
        print('classB: init')
        self.val = np.random.randint(1000)

    def __new__(cls, *args, **kwargs):
        print('classB: new')
        return super(ClassB, cls).__new__(cls, *args, **kwargs)

    def __call__(cls, *args, **kwargs):
        print('classB: call')
        return super(ClassB, cls).__call__(*args, **kwargs)


class ClassC(ClassB):

    def __init__(self, *args, **kwargs):
        print('classC: init')

        super(ClassC, self).__init__(self, *args, **kwargs)
        self.test = 3

    def __new__(cls, *args, **kwargs):
        print('classC: new')
        return super(ClassC, cls).__new__(cls, *args, **kwargs)

    def __call__(cls, *args, **kwargs):
        print('classC: call')
        return super(ClassC, cls).__call__(*args, **kwargs)



if __name__ == '__main__':


    a1 = ClassA()
    b1 = ClassB()
    a2 = ClassA()
    b2 = ClassB()
    c1 = ClassC()

    print(a1.val)
    print(b1.val)
    print(a2.val)
    print(b2.val)

I have two questions about it:

Upvotes: 2

Views: 1257

Answers (1)

bruno desthuilliers
bruno desthuilliers

Reputation: 77912

First, here's a stripped-down version of your code with some "noise" removed for readability.

import random

class Meta(type):
    _instance = None

    def __call__(cls, *args, **kwargs):
        print('meta: call: %s' % cls)
        if cls._instance is None:
            cls._instance = super(Meta, cls).__call__(*args, **kwargs)
        print("meta: call: returning %s@%s" % (cls._instance.__class__, id(cls._instance)))
        return cls._instance


class ClassA(object):
    __metaclass__ = Meta

    def __new__(cls, *args, **kwargs):
        print('classA: new')
        return super(ClassA, cls).__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        self.val = random.randint(1, 1000)
        print('classA: init')


class ClassB(object):
    __metaclass__ = Meta

    def __new__(cls, *args, **kwargs):
        print('classB: new')
        return super(ClassB, cls).__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        print('classB: init')
        self.val = random.randint(1, 1000)

class ClassC(ClassB):
    def __new__(cls, *args, **kwargs):
        print('classC: new')
        return super(ClassC, cls).__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        print('classC: init')
        super(ClassC, self).__init__(self, *args, **kwargs)
        self.test = 3


def main():
    a1 = ClassA()
    b1 = ClassB()
    a2 = ClassA()
    b2 = ClassB()
    c1 = ClassC()

    print(a1.val)
    print(b1.val)
    print(a2.val)
    print(b2.val)

if __name__ == '__main__':
    main()

Now your questions:

Why does cls._instance is main.ClassA object (or main.ClassA object)? Should it not be a parent instance as it is created with super?

No, why ? In your case, when calling ClassA(), you actually invoke Meta.__call__(ClassA). super(Meta, cls) will select the next class in Meta.__mro__, which is type, and so invoke type.__call__(Meta, ClassA). type.__call__(Meta, ClassA) will in turn invoke ClassA.__new__() and ClassA.__init__(). So of course (hopefully, else you could hardly use inheritance at all), you do get a ClassA instance back.

As a general rule: super(CurrentClass, obj_or_cls).something() will select the something implementation in the "parent" class (actually in the next class in the __mro__) but the self (or cls or whatever) first argument will still point to self (or cls etc). IOW, you are selecting a parent implementation of a method, but this method will still get the current object/class as first argument.

Why nothing (neither __call__ nor __init__) inside ClassC is called when creating a ClassC instance but only the ClassB.__call__ (as it inherits from it)

ClassC.__call__() is not called because nothing in your code calls it. It's ClassC.__metaclass__.__call__() which is responsible for ClassC instanciation. You'd need to call an instance of ClassC to have ClassC.__call__() invoked.

ClassC.__init__() is not invoked because actually no instance of ClassC is ever created. The reason here is that you first create an instance of ClassB. At this point (first instanciation of ClassB), ClassB has no attribute _instance, so the attribute is looked up on it's parent class object. Since object has no attribute _instance either, the lookup continues on ClassB.__class__, which is Meta. This one does have an _instance attribute, which is None, so you do create a ClassB instance and bind it to ClassB._instance (sus creating the attribute in ClassB.__dict__).

Then when you try to instanciate ClassC, the if cls._instance is None test will first lookup _instance on ClassC. At this point, ClassC does not have an _instance attribute, so it's looked up on ClassC first parent (next class in the mro), which is ClassB. Since you already instanciated ClassB once, the lookup ends here, resolving ClassC._instance to ClassB.__dict__["_instance"] , so what you get back is the already created ClassB instance.

If you want a working implementation, you have to get rid of Meta._instance and either set _instance to None on each class in Meta.__init__():

class Meta(type):
    def __init__(cls, *args, **kw):
        cls._instance = None

    def __call__(cls, *args, **kwargs):
        print('meta: call: %s' % cls)
        if cls._instance is None:
            cls._instance = super(Meta, cls).__call__(*args, **kwargs)
        print("meta: call: returning %s@%s" % (cls._instance.__class__, id(cls._instance)))
        return cls._instance

or replace the test in Meta.__call__() with a getattr():

class Meta(type):

    def __call__(cls, *args, **kwargs):
        print('meta: call: %s' % cls)
        if not getattr(cls, "_instance", None):
            cls._instance = super(Meta, cls).__call__(*args, **kwargs)
        print("meta: call: returning %s@%s" % (cls._instance.__class__, id(cls._instance)))
        return cls._instance

Upvotes: 2

Related Questions