Multiple Inheritance: missing required positional arguments that aren't actually required

Viewed 366

class A:
    def __init__(self, arg):
        print("hit A")
        self.arg = 1
        print("close:A")
        return

class B(A):
    def __init__(self, arg, arg1):
        print("hit B")
        super().__init__(arg)
        self.arg1 = arg1
        print("close B")
        return

class D(A):
    def __init__(self, arg, arg1):
        print("hit D")
        super().__init__(arg)
        self.arg1 = 3
        print("done D")
        return

class C(D, B):
    def __init__(self, arg, arg1):
        print("hit C")
        super().__init__(arg, arg1)
        print("done C")
        return


c = C(1,2)

Throws a TypeError when D calls A.init because of missing required positional argument: arg1.

I am aware of how to get around this situation but my question is: why does this happen? If you flip the ordering of D and B, the error gets thrown in B so it is happening regardless of order and has to be related to how D and B are written but just passing arg1 to A makes no sense (although that does it solve it). I am obviously not understanding something about super() or multiple inheritance in Python.

(I am never doing this again btw. This is based on some real-life code, I wrote this up using composition in another language, and am aware that whatever I did here wasn't a good idea.)

1 Answers

When you call super in Python, the language uses a specific Method Resolution Order to identify which method to call. In particular, although we like to visualize our inheritance hierarchy as a tree, internally it ends up getting linearized so that Python knows what order to call methods in. The specific linearization procedure can be seen in more detail in the linked article, but in your case the MRO is

C, D, B, A, object

To summarize the procedure (again, I'm glossing over a lot here), we start with C. That's the head of our list. Then its first parent D goes next. Then D's first parent would go next. But D's first parent is A, which appears in B's hierarchy as well, so in order to get a topological ordering we need to omit A right now. Then we move onto B, which is next in our list. Then we proceed to B's parent, which is A. A doesn't appear anywhere else not already included in our list, so we can include it. Finally, we include A's parent object, and we're done.

So when you call super from D, you're actually calling B's initializer, because B is next in the Method Resolution Order.

If this was any function other than an initializer, it would be in poor taste to change the argument count, as a violation of Liskov substitution. For initializers specifically, we simply need to take extra care when using multiple inheritance that this particular situation doesn't come up.

Related