I have the class structure below. Just ignore the minor issues such as mismatching variable names, this is not the issue here, I’ll explain what is in a moment.
class A:
def __init__(self, a, b, c, d=0, e=0, f=0):
self.a = a
self.b = b
self.c = c
self.d = d
self.e = e
self.f = f
class B(A):
def __init__(self, a, b, c, d, **kwargs):
super().__init__(a, b, d, **kwargs)
# A.__init__(self, a, b, d, **kwargs)
self.c = c
class C(A):
def __init__(self, a, b, c, d, **kwargs):
super().__init__(a, b, d, **kwargs)
# A.__init__(self, a, b, d, **kwargs)
self.c = c
class D(B, C):
def __init__(self, a, b, c, d, e, **kwargs):
super().__init__(a, b, c, e, **kwargs)
# B.__init__(self, a, b, c, e, **kwargs)
C.__init__(self, a, b, d, e)
class E(D):
def __init__(self, a, b, c, d, e=0, **kwargs):
super().__init__(a, b, c, d, e, **kwargs)
# D.__init__(self, a, b, c, d, e, **kwargs)
if __name__ == '__main__':
E(None, None, None, 0)
When I run this code, it gives the error below:
Traceback (most recent call last):
File "/Users/user/Library/Application Support/JetBrains/PyCharm2023.2/scratches/scratch_5.py", line 39, in <module>
E(None, None, None, 0)
File "/Users/user/Library/Application Support/JetBrains/PyCharm2023.2/scratches/scratch_5.py", line 34, in __init__
super().__init__(a, b, c, d, e, **kwargs)
File "/Users/user/Library/Application Support/JetBrains/PyCharm2023.2/scratches/scratch_5.py", line 27, in __init__
super().__init__(a, b, c, e, **kwargs)
File "/Users/user/Library/Application Support/JetBrains/PyCharm2023.2/scratches/scratch_5.py", line 13, in __init__
super().__init__(a, b, d, **kwargs)
TypeError: C.__init__() missing 1 required positional argument: 'd'
However, if I replace each of the super().__init__()
calls with the first superclass.__init__
(which is apparently not the same thing or otherwise both cases should fail), it works perfectly fine.
To make it work, just uncomment all super().__init__()
calls and uncomment all commented lines, it will work perfectly fine. The question is why both cases don’t fail? and how does super().__init__
work differently? doesn’t super()
resolve to the first superclass? is the simplified logic below wrong?
class A(B, C)
def __init__():
super().__init__() # is this not equivalent to B.__init__()?
3
Python super
is funky; it finds the next class in the method resolution order (MRO) of type(self)
, not the first “superclass” as you mean it.
When you call super().__init__
in B.__init__
, super
looks for the next class in the MRO of self
, which is of type E
. If you look at E.__mro__
, it’s something like:
(<class '__main__.E'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
The class after B
here is C
, so C.__init__
is called.
This is documented in the official Python docs for super
.
To answer your edit: the simplified logic is correct. However, the way Python looks up super().__init__
inside B.__init__
is not the way you might have expected.
When you are instantiating E
, the following chain of inheritance occurs:
E -> D -> B -> C -> A
This is called the method resolution order (MRO). What you will notice is that this is different than the MRO for the class B
, which is just:
B -> A
without class C
in between. The addition of C
to the MRO is because of the multiple inheritance. This means that when instantiating E
, the call to super()
within the class B
references class C
and NOT class A
.
This is because the self
that gets passed in to each super().__init__
is from the original instantiating class, in this case E
.
Here is an example to show the MRO:
class A:
def __init__(self, a):
print('A:', [c.__name__ for c in self.__class__.mro()])
self.a = a
class B(A):
def __init__(self, a):
print('B:', [c.__name__ for c in self.__class__.mro()])
super().__init__(a)
class C(A):
def __init__(self, a):
print('C:', [c.__name__ for c in self.__class__.mro()])
super().__init__(a)
class D(B, C):
def __init__(self, a):
print('D:', [c.__name__ for c in self.__class__.mro()])
super().__init__(a)
class E(D):
def __init__(self, a):
print('E:', [c.__name__ for c in self.__class__.mro()])
super().__init__(a)
E(0)
# prints:
# E: ['E', 'D', 'B', 'C', 'A', 'object']
# D: ['E', 'D', 'B', 'C', 'A', 'object']
# B: ['E', 'D', 'B', 'C', 'A', 'object']
# C: ['E', 'D', 'B', 'C', 'A', 'object']
# A: ['E', 'D', 'B', 'C', 'A', 'object']
The MRO does not change through the entire super
stack. When each class calls super()
, if finds itself in the MRO, and then returns a reference to the next object in the MRO chain. This is why the behavior of super()
in B
is different when instantiating E
versus instantiating B
directly.
Here is what B
creates:
B(0)
# prints:
# B: ['B', 'A', 'object']
# A: ['B', 'A', 'object']
How to fix it?
In order for D
to inherit from both B
and C
, then C
would need to have the same init parameters as A
.
If you really need your inputs for A
and C
to be different, you can check the MRO in B
and modify the parameters on the fly. Keep in mind, this greatly increases how tightly coupled your code is, and is very hard to maintain.
class B(A):
def __init__(self, a, b, c, d, **kwargs):
mro = self.__class__.mro()
ix = mro.index(B)
if mro[ix+1] == A:
super().__init__(a, b, d, **kwargs)
else:
super().__init__(a, b, c, d, **kwargs)
self.c = c