In my program, I am using abstract base classes to force subclasses to have certain attributes and/or behavior. I am aware this is not particularly pythonic, but the complexity of my code doesn't lend itself to duck-typing, and either way it is a bit late to change the approach. This means that when a class needs to implement two of these base classes, I have them inherit from both of them. There, I am getting the problem that my child class needs to correctly call both parents' init methods with correct arguments, but the way super() seems to work doesn't lend itself to this easily. I would like to know whether there is a way to do this without calling init of the base classes directly.
I have found many posts and articles about this topic, and some seem to say it is possible, sometimes with a vague description, but none of the sources I have found directly answers how to do this.
Let's say I have two base classes
class Base1:
def __init__(self, base_1: int, **kwargs):
print(f"Base 1 initialized with base_1={base_1} and **kwargs={kwargs}")
self._base_1: int = base_1
class Base2:
def __init__(self, base_2: int, **kwargs):
print(f"Base 2 initialized with base_2={base_2} and **kwargs={kwargs}")
self._base_2: int = base_2
I would like to be able to implement a child class that inherits from both base classes that calls both init methods like so
class Child(Base1, Base2):
def __init__(self, base_1: int, base_2: int):
Base1.__init__(self, base_1=base_1)
Base2.__init__(self, base_2=base_2)
but using super().
I have tried many versions. Either the mro leads to the wrong init method or the mro leads to calling one of the init methods, but not the other, like here:
# With this code, child = Child(base_1=1, base_2=2) prints only:
# "Base 1 initialized with base_1=1 and **kwargs={'base_2': 2}"
class Child(Base1, Base2):
def __init__(self, base_1: int, base_2: int):
super().__init__(base_1=base_1, base_2=base_2)
I understand why this code fails, but not what to do about it.
Can someone explain how to handle this scenario? Or is
Base1.__init__(self, base_1=base_1)
Base2.__init__(self, base_2=base_2)
the only way?
Make your super classes call super.__init__
with the remaining kwargs:
class Base1:
# def __init__(self, base_1: int, **kwargs): # enforces
def __init__(self, base_1: int, **kwargs):
super().__init__(**kwargs)
print(f"Base 1 initialized with base_1={base_1} and **kwargs={kwargs}")
self._base_1: int = base_1
class Base2:
def __init__(self, base_2: int, **kwargs):
super().__init__(**kwargs)
print(f"Base 2 initialized with base_2={base_2} and **kwargs={kwargs}")
self._base_2: int = base_2
class Child(Base1, Base2):
def __init__(self, *, base_1: int, base_2: int):
super().__init__(base_1=base_1, base_2=base_2)
c = Child(base_1=1, base_2=2)
# Base 2 initialized with base_2=2 and **kwargs={}
# Base 1 initialized with base_1=1 and **kwargs={'base_2': 2}
This way, the mro of the actual self
object's class will be able to traverse the ancestral tree with a single super call.
The *,
in the child class' constructor signature enforces named arguments to disallow calls like Child(1, 2)
which would obfuscate the target attributes and lead to buggy behavior if you were to ever change the order of super classes.
Note that you can't pass random additional kwargs because the last super will call the object
constructor which does not take any [kw]args:
b = Base1(base_1=1, foo=5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in __init__
TypeError: object.__init__() takes exactly one argument (the instance to initialize)