In Python, I have noticed that the __init__
constructor of a class does not implicitly call the __init__
constructor of its base class. Indeed, this program:
class A:
def __init__(self): print("A")
class B(A):
def __init__(self): print("B")
b = B()
outputs:
B
This contrasts with C++ for which the constructor of a class implicitly calls the default constructor of its base class. Indeed, this program:
#include <iostream>
struct A {
A() { std::cout << "A" << std::endl; }
};
struct B: A {
B() { std::cout << "B" << std::endl; }
};
int main() {
B b {};
return 0;
}
outputs:
A
B
I see two benefits with the behaviour of Python: __init__
behaves like any other methods and everything is explicit.
But I see also one drawback: we can get runtime errors (AttributeError
) when a class calls inherited methods that use uninitialised attributes, because the constructor of the class did not explicitly call the constructor of its base class. Indeed, this program:
class A:
def __init__(self): self.x = 0
def f(self): return self.x
class B(A):
def __init__(self): pass
b = B()
b.f()
outputs:
[…]
AttributeError: 'B' object has no attribute 'x'
The problem is obvious in this program since we can see the implementation of the base class, but when we subclass a base class defined in another module I imagine it could be more pernicious.
For explicitly calling the __init__
constructor of a base class, instead of using the base class directly we can use the super
class in order to allow cooperative multiple inheritance at the same time. Like explained in Raymond Hettinger’s excellent article Python’s super() considered super!, this requires:
- calling
super().__init__
in each__init__
constructor of the inheritance graph; - matching caller arguments and callee parameters (by having each
__init__
constructor take the keyword parameters that it uses and a variadic keyword argument that it forwards to thesuper().__init__
base constructor).
Indeed, this program:
class A:
def __init__(self, a, **kwargs):
print("A", a, kwargs)
super().__init__(**kwargs)
class B(A):
def __init__(self, b, **kwargs):
print("B", b, kwargs)
super().__init__(**kwargs)
class C(A):
def __init__(self, c, **kwargs):
print("C", c, kwargs)
super().__init__(**kwargs)
class D(B, C):
def __init__(self, d, **kwargs):
print("D", d, kwargs)
super().__init__(**kwargs)
D(a=1, b=2, c=3, d=4)
outputs:
D 4 {'a': 1, 'b': 2, 'c': 3}
B 2 {'a': 1, 'c': 3}
C 3 {'a': 1}
A 1 {}
If Python was designed in such a way that the __init__
constructor of a class implicitly called the __init__
constructor of its base class, the previous program would look like this:
class A:
def __init__(self, a):
print("A", a)
class B(A):
def __init__(self, b):
print("B", b)
class C(A):
def __init__(self, c):
print("C", c)
class D(B, C):
def __init__(self, d):
print("D", d)
D(a=1, b=2, c=3, d=4)
Finally here are my questions:
- Would such a language design be possible?
- Would such a language design be desirable?