Multiple Inheritance with kwargs

Viewed 738

Problem

I came across this code in Object Oriented Programming by Dusty Phillips (simplified for brevity) and I unsure about a specific part of this definition.

class A:
    def __init__(self, a, **kwargs):
        super().__init__(**kwargs)
        self.a = a

class B:
    def __init__(self, b, **kwargs):
        super().__init__(**kwargs)
        self.b = b

class C(A, B):
    def __init__(self, c, **kwargs):
        super().__init__(**kwargs)
        self.c = c

Questions

  1. Since the method resolution order is (__main__.C, __main__.A, __main__.B, object), could class B be defined in the following way instead?
class B:
    def __init__(self, b):
        self.b = b
  1. Isn't super().__init__(**kwargs) in class B redundant, since any surplus kwargs passed to C will be passed to object, raising?

TypeError: object.__init__() takes exactly one argument (the instance to initialize)

  1. Is this a safeguard for if C was defined as class C(B, A) instead of class C(A, B)?
3 Answers

Consider how you might instantiate C:

c = C(a=3, b=5, c=9)

C.__init__ gets all the keyword arguments, but only uses the one for its own parameter c. The rest are passed on for the next __init__ method in the chain. In this case, that's A.__init__, which "pulls out" the argument for a and passes b on to B.__init__. B uses that and passes on the (now-empty) set of keyword arguments to the next method, object.__init__. Because all keyword arguments have been "claimed" and processed by other classes, object.__init__ succeeds.

Because of how the MRO is constructed, classes that properly use super() guarantee collectively that **kwargs will be empty by the time object.__init__ is called.

In this example B would have worked the same if it was defined as you say (point 1) and C is as is (and there is no other use of it).

As for point 2: A call to constructor of super() would indeed fail as indicated, if there were still keyword arguments left, e.g.:

c = C(a=1, b=2, c=3, d=4)
# -> TypeError: object.__init__() takes exactly one argument (the instance to initialize)

As the class B is (originally) written, it would be also OK if used in reverse order as you say, or if there was one (or) more super-class(es), e.g.:

class D:
    def __init__(self, d, **kwargs):
        super().__init__(**kwargs)
        self.d = d

class C(A,B,D):
    ...

Since the method resolution order is (__main__.C, __main__.A, __main__.B, object), could class B be defined in the following way instead?

No, because then this would fail:

class D:
    def __init__(self, d, **kwargs):
        self.d = d
        super().__init__(**kwargs)

class E(C, D):
    def __init__(self, e, **kwargs):
        self.e = e
        super().__init__(**kwargs)

The MRO of E is (E, C, A, B, D, object), so B must call super().__init__ otherwise D.__init__ won't be called.

Isn't super().__init__(**kwargs) in class B redundant, since any surplus kwargs passed to C will be passed to object, raising?

No, because the surplus kwargs will go to D.__init__ in the above example. But even without that, it is not redundant to raise an error when you call a constructor with too many arguments; it is desirable to have an error message informing you about your incorrect code, rather than for the mistake to go undetected.

Is this a safeguard for if C was defined as class C(B, A) instead of class C(A, B)?

In some sense, sure; but really it's a safeguard for B occurring in any class hierarchy, so long as the other classes in the hierarchy follow the same rule of calling super().__init__(**kwargs).

Related