Search code examples
pythonmultiple-inheritance

Maintaining readability when using super() for direct multiple inheritance


For the case of the most basic multiple inheritance:

class A:
    def __init__(self, a):
        self.a = a

class B:
    def __init__(self, b):
        self.b = b

class C(A, B):
    def __init__(self, a, b): 
        A.__init__(self, a)
        B.__init__(self, b)

I do not see why super() should be used. I suppose you could implement it with kwargs, but that is surely less readable than the above method. I am yet to find any answers on stack overflow which are in favour of this method, yet surely for this case it is the most satisfactory?

There are a lot of questions marked as duplicate on this topic, but no satisfactory answers for this exact case. This question addresses multiple inheritance and the use of super() for a diamond inheritance. In this case there is no diamond inheritance and neither parent class have any knowledge of each other, so they shouldn't need to call super() like this suggests.

This answer deals with the use of super in this scenario but without passing arguments to __init__ like is done here, and this answer deals with passing arguments but is again a diamond inheritance.


Solution

  • One correct way to use super here would be

    class A:
        def __init__(self, a, **kwargs):
            super().__init__(**kwargs)
            self.a = a
    
    
    class B:
        def __init__(self, b, **kwargs):
            super().__init__(**kwargs)
            self.b = b
    
    
    class C1(A, B):
        pass
    
    
    class C2(A, B):
        def __init__(self, a, b, **kwargs):
            super().__init__(a=a, b=b, **kwargs)
    
    
    c1 = C1(a="foo", b="bar")
    c2 = C2(a="foo", b="bar")
    

    The method resolution order for C is [C, A, B, object]. Each time super() is called, it returns a proxy for the next class in the MRO, based on where super() is called at the time.

    You have two options when defining C, depending on whether you want to define C.__init__ with a signature that mentions the two arguments A and B required for initialization. With C1, C1.__init__ is not defined so A.__init__ will be called instead. With C2, you need to explicitly call the next __init__ method in the chain.

    C, knowing that it is a subclass of A and B, has to at least provide the expected arguments for the known upstream __init__ methods.

    A.__init__ will pass on everything except a to the next class's __init__ method.

    B.__init__ will pass on everything it receives except b.

    object.__init__ will finally be called, and assuming all previous classes correctly removed the keyword arguments they introduced, will receive no additional arguments.

    Changing the order in which the various __init__s are called means changing the MRO, which means altering the order of the base classes. If you want more control than that, then cooperative multiple inheritance is not for you.