Resolving Diamond Inheritance within Python Classes

18.1k views Asked by At

Consider the following python code:

class Parent(object):
    def __init__(self, name, serial_number):
        self.name = name
        self.serial_number = serial_number


class ChildA(Parent):
    def __init__(self, name, serial_number):
        self.name = name
        self.serial_number = serial_number
        super(ChildA, self).__init__(name = self.name, serial_number = self.serial_number)

    def speak(self):
        print("I am from Child A")


class ChildB(Parent):
    def __init__(self, name, serial_number):
        self.name = name
        self.serial_number = serial_number
        super(ChildB, self).__init__(name = self.name, serial_number = self.serial_number)

    def speak(self):
        print("I am from Child B")


class GrandChild(ChildA, ChildB):
    def __init__(self, a_name, b_name, a_serial_number, b_serial_number):
        self.a_name = a_name
        self.b_name = b_name
        self.a_serial_number = a_serial_number
        self.b_serial_number = b_serial_number
        super(GrandChild, self).__init_( something )

When running the super function in GrandChild, what is the proper way to format the __init__ arguments so that ChildA and ChildB both get the correct arguments?

Also how do you access the two different versions of the speak method (ChildA's version and ChildB's version) from within the GrandChild class?

3

There are 3 answers

1
Sam Hartman On BEST ANSWER

so, when you call super from the grandchild, ChildA's __init__ method will be called because super follows the __mro__ property (parents left to right then grandparents left-to-right, then great grandparents, ...)

Since ChildA's init also calls super, then all the super calls will be chained, calling child b's __init__ and eventually the parent init.

For that to work, your interface generally needs to be consistent. That is positional arguments need to mean the same things, and be in the order.

In situations where that's not the case, keyword arguments may work better.

class Parent:    
    def __init__(self, name, serial, **kwargs):
        self.name = name
        self.serial = serial

class ChildA(Parent):    
    def __init__(self, a_name, a_serial, **kwargs):
        self.a_name = a_name
        self.a_serial = a_serial
        super().__init__(**kwargs)

class ChildB(Parent):    
    def __init__(self, b_name, b_serial, **kwargs):
        self.b_name = b_name
        self.b_serial = b_serial
        super().__init__(**kwargs)


class GrandChild(ChildA, ChildB):
    def __init__(self):
        super().__init__(name = "blah", a_name = "a blah", b_name = "b blah", a_serial = 99, b_serial = 99, serial = 30)

Also note that in your code name and serial are reused as instance properties between all the classes and that's probably not what you want.

3
Niobos On

In python, you can explicitly call a particular method on (one of) your parent class(es):

ChildA.__init__(self, a_name, a_serial)
ChildB.__init__(self, b_name, b_serial)

Note that you need to put the self in explicitly when calling this way.

You can also – as you did – use the super() way, which will call the "first" parent. The exact order is dynamic, but by default it will do left-to-right, depth-first, pre-order scans of your inheritance hierarchy. Hence, your super() call will only call __init__ on ChildA.

0
Vivek G. On

The Diamond problem doesn’t exist in Python because it gives preference to the class that gets inherited first.

Two important rules:

  1. We go from Left to Right.

  2. We goto the parent when all the children are considered.

    class Parent:    
        def __init__(self, name, serial):
            self.name = name
            self.serial = serial
    
        def printName(self):
            print("Inside P");
    
    class ChildA(Parent):    
        def __init__(self, a_name, a_serial):
            self.a_name = a_name
            self.a_serial = a_serial
    
        def printName(self):
            print("Inside A");
    
    class ChildB(Parent):    
        def __init__(self, b_name, b_serial):
            self.b_name = b_name
            self.b_serial = b_serial
    
        def printName(self):
            print("Inside B");
    
    
    class GrandChild(ChildA, ChildB):
        def __init__(self):
            super().__init__(name = "blah", a_name = "a blah", b_name = "b blah", a_serial = 99, b_serial = 99, serial = 30)
    
    obj = GrandChild();
    obj.printName();
    

    Output is Inside A

    In the above example, GrandChild(ChildA, ChildB) denotes that class GrandChild inherited class ChildA first, followed by class ChildB.

    If you switch places of an inherited child in GC class then:

    class GrandChild(ChildB, ChildA):
        def __init__(self):
            super().__init__(name = "blah", a_name = "a blah", b_name = "b blah", a_serial = 99, b_serial = 99, serial = 30)
    
    obj = GrandChild();
    obj.printName();
    

    Output is Inside B