pythonmethod-resolution-order

Method Resolution Order (MRO) in Python: super multiple inheritance


I got the following example code. I understand that the code is falling since super(A, self).__init__(attr_base1, attr_base2) is calling the __init__ of B(Base), but I don't really understand why. Since I put A in super(A, self), I thought that it should search for the parent class starting from A. What should I do if I want A.init(self, attr1, "b1", "b2") to call in super for the Base __init__?

class Base(object):
    def __init__(self, attr_base1, attr_base2):
        self.attr_base1 = attr_base1
        self.attr_base2 = attr_base2

    def foo(self):
        print ('A.foo()')

class A(Base):
    def __init__(self, attr, attr_base1, attr_base2):
        super(A, self).__init__(attr_base1, attr_base2)
        self.attr1 = attr

class B(Base):
    def __init__(self, attr):
        self.attr2 = attr

    def foo(self):
        print ('B.foo()')

class C(A, B):
    def __init__(self, attr1, attr2):
        A.__init__(self, attr1, "b1", "b2")
        A.__init__(self, attr2)

c = C(1, 2)

Solution

  • I would redefine all three classes to make them work better in a cooperative multiple-inheritance hierarchy. (See https://rhettinger.wordpress.com/2011/05/26/super-considered-super/ for more information about the design choices made here.)

    Some general rules for __init__:

    1. Accept arbitrary keyword arguments.
    2. Always call super().__init__ with the extra keyword arguments, even if you are only statically inheriting from object.
    3. Use keyword-only parameters with __init__
    4. Make sure parameter names are distinct across the classes.
    5. If you try to inherit base classes that violate any of the above rules, you may need to write adaptor classes that wrap the offending classes and inherit from the adaptors instead.
    class Base:
        def __init__(self, *, attr_base1, attr_base2, **kwargs):
            super().__init__(**kwargs)
            self.attr_base1 = attr_base1
            self.attr_base2 = attr_base2
    
        def foo(self):
            print ('A.foo()')
    
    
    class A(Base):
        def __init__(self, *, a_attr, **kwargs):
            super().__init__(**kwargs)
            self.attr1 = a_attr
    
    
    class B(Base):
        def __init__(self, *, b_attr, **kwargs):
            super().__init__(**kwargs)
            self.attr2 = b_attr
    
        def foo(self):
            print ('B.foo()')
    
    
    class C(A, B):
        def __init__(self, **kwargs):
            # Override any explicitly passed values.
            kwargs["attr_base1"] = "b1"
            kwargs["attr_base2"] = "b2"
    
            super().__init__(**kwargs)
    
    
    c = C(a_attr=1, b_attr=2)
    

    In this example,

    1. C.__init__ is responsible for hard-coding the base-class attributes before delegating the actual initialization to A.__init__.
    2. A.__init__ is responsible for extracting a_attr from the keyword arguments and initializing the attribute attr1 before delegating the rest of the initialization to B.__init__.
    3. B.__init__ is responsible for extracting b_attr fro the keyword arguments and initializing the attribute attr2 before delegating the rest of the initialization to Base.__init__.
    4. Base.__init__ is responsible for extracting attr_base1 and attr_base2 from the keyword arguments, and initializing the attributes attr_base1 and attr_base2 before delegating the rest of the initialization to object.__init__.
    5. object.__init__ doesn't really do anything except produce an exception if any "unclaimed" keyword arguments get passed to it.