Reputation: 2321
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:
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
?
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)?
Upvotes: 2
Views: 1257
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 theClassB.__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