Search code examples
python-3.xmultiple-inheritancesuperclass

Pythonic way of passing different arguments in multiple inheritance setup


Consider the following snippet:

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

class B:
    def __init__(self, b1, b2):
        self._b1 = b1
        self._b2 = b2

class C(B, A):
    def __init__(self, b1, b2, a):
        super().__init__(b1=b1, b2=b2, a=a)

Then I will encounter the following error:

TypeError: init() got an unexpected keyword argument 'a'

To resolve it, I initilize the superclass 'A', as follows:

class C(B, A):
    def __init__(self, b1, b2, a):
        super().__init__(b1=b1, b2=b2)
        A.__init__(self, a=a)

Yet another solution:

class C(B, A):
    def __init__(self, b1, b2, a):
        super().__init__(b1=b1, b2=b2)
        super(B, self).__init__(a=a)

I found these solutions in a try and error way. So, I'm wondering what is the most elegant way of passing arguments to multiple superclasses.


Solution

  • If you have classes that will be composed with other classes that they don't need to know about (specifically, don't need or don't care about their arguments), you can use the ** Python Parameter/argument passing syntax to fold and unfold arguments into a dictionary.

    In other words, your code would be like:

    class A:
        def __init__(self, a, **kwargs):
            self._a = a
            super().__init__(**kwargs)
    
    class B:
        def __init__(self, b1, b2, **kwargs):
            self._b1 = b1
            self._b2 = b2
            super().__init__(**kwargs)
    
    class C(B, A):
        def __init__(self, b1, b2, a):
            super().__init__(b1=b1, b2=b2, a=a)
    

    Also note that in your base-classes __init__ code above you were not calling super(), so just the __init__ of the closer super-class would ever be executed.

    super() in Python do all the magic needed for multiple-inheritance to work correctly. One of the most complete articles on that is still Python's super considered Super.

    In short terms what takes place: whenever you create a class, using or not multiple parents, Python creates a __mro__ attribute to it - this is the "method resolution order" for that class, indicating the order that methods and attributes will be searched for in the ancestors.

    The algorithm for calculating the MRO itself is complex, a bit thought to explain, but which can be reasonably trusted as it "just does the right thing". Up to today the only place I found it described in full is on its original presentation in Python 2.3 documentation, more than 15 years ago. (optional reading,as it "does the right thing".)

    What super() does is to create a proxy object that will pick the next class in the "mro" sequence for the original calling class, and search methods directly in its __dict__ - if not found, it will go the next entry on the "mro". So, if you only consider the class B, super().__init__() inside its body would call object.__init__. If "kwargs" is empty at this point, as it will be if B() is called with only the parameters it cares about, that is just what is wanted.

    When B.__init__ is run in a chainned super() call from a class "C" composed with "B" and "A", whoever, the super() in B.__init__ will be using "C"'s MRO - and the next class is "A" - so A.__init__ is called with all unconsumed keyword arguments.