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?