Search code examples
pythonpython-3.xmultiple-inheritance

How does Python pass __init__ parameters with multiple inheritance


I have this code, showing a classic diamond pattern:

class A:
    def __init__( self, x ):
        print( "A:" + x )


class B( A ):
    def __init__( self, x ):
        print( "B:" + x )
        super().__init__( "b" )


class C( A ):
    def __init__( self, x ):
        print( "C:" + x )
        super().__init__( "c" )


class D( B, C ):
    def __init__( self ):
        super().__init__( "d" )


d = D()

The output is:

B:d
C:b
A:c
  • B:d makes sense, since D derives from B.
  • The A:c I almost get, though I could equally see A:b.
  • However, the C:b bit doesn't make sense: C does not derive from B.

Could someone explain?

Questions such as this unfortunately do not mention the parameters.


Solution

  • Classes in Python are dynamically composed - that includes inheritance.

    The C:b output does not imply that B magically inherits from C. If you instantiate either B or C, none knows about the other.

    >>> B('root')
    B:root
    A:b
    

    However, D does know about both B and C:

    class D(B,C):
        ...
    

    There is a lot of technicalities available on this. However, there are basically two parts in how this works:

    1. Direct Base Classes are resolved in order they appear.
      • B comes before C.
    2. Recursive Base Classes are resolved to not duplicate.
      • A Base Class of both B and C must follow both.

    For the class D, that means the base classes resolve as B->C->A! C has sneaked in between B and A - but only for class D, not for class B.


    Note that there is actually another class involved: all classes derive from object by default.

    >>> D.__mro__
    (__main__.D, __main__.B, __main__.C, __main__.A, object)
    

    You have already written A knowing that there is no base to take its parameters. However, neither B nor C can assume this. They both expect to derive from an A object. Subclassing does imply that both B and C are valid A-objects as well, though!

    It is valid for both B and C to precede B and C, since the two are subclasses of A. B->C->A->object does not break that B expects its super class to be of type A.

    With all other combinations, one ends up with C preceding nothing (invalid) or object preceding something (invalid). That rules out depth-first resolution B->A->object->C and duplicates B->A->object->C->A->object.


    This method resolution order is practical to enable mixins: classes that rely on other classes to define how methods are resolved.

    There is a nice example of how a logger for dictionary access can accept both dict and OrderedDict.

    # basic Logger working on ``dict``
    class LoggingDict(dict):
        def __setitem__(self, key, value):
            logging.info('Settingto %r' % (key, value))
            super().__setitem__(key, value)
    
    # mixin of different ``dict`` subclass
    class LoggingOD(LoggingDict, collections.OrderedDict):
        pass