Search code examples
pythonpython-3.xmultiple-inheritancesuper

How can I get a class using multiple inheritance to call all parents' init methods with arguments?


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?


Solution

  • 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)