2

In a multiple inheritance scenario, how does Python3 pass the arguments to all the parent classes? Consider the toy program below:

class A(object):
    def __init__(self, a='x', **kwargs):
        print('A', kwargs)
        super().__init__()

class B(object):
    def __init__(self, **kwargs):
        print('B', kwargs)
        super().__init__()

class C(A,B):
    def __init__(self, **kwargs):
        print('C', kwargs)
        super().__init__(**kwargs)

c = C(a='A', b='B', c='C')

The output is:

C {'a': 'A', 'b': 'B', 'c': 'C'}
A {'b': 'B', 'c': 'C'}
B {}

What I expect to do is to pass the same kwargs to all the parent classes and let them use the values as they want. But, what I am seeing is that once I'm down the first parent class A, the kwargs is consumed and nothing is passed to B.

Please help!


Update If the order of inheritance was deterministic and I knew the exhaustive list of kwargs that can be passed down the inheritance chain, we could solve this problem as

class A(object):
    def __init__(self, a='x', **kwargs):
        print('A', kwargs)
        super().__init__(**kwargs)

class B(object):
    def __init__(self, **kwargs):
        print('B', kwargs)
        super().__init__()

class C(A,B):
    def __init__(self, **kwargs):
        print('C', kwargs)
        super().__init__(**kwargs)

c = C(a='A', b='B', c='C')

Here, since A passes down kwargs and we know that B would be called after it, we are safe, and by the time object.__init__() is invoked, the kwargs would be empty.

However, this may not always be the case.

Consider this variation:

class C(B,A):
    def __init__(self, **kwargs):
        print('C', kwargs)
        super().__init__(**kwargs)

Here, object.__init__() invoked from class A would raise an exception since there are still kwargs left to consume.

So, is there a general design guideline that I should be following?

Anshul
  • 746
  • 9
  • 22
  • 2
    Well in `A` you used `super().__init__()` but if you wanted to pass them, you would use `super().__init__(a=a, **kwargs)` instead. It's up to you to pass them along (or not), no magic here. – wim Jan 11 '23 at 04:50
  • Check if the answer available here may help : https://stackoverflow.com/questions/60495296/multiple-inheritance-with-kwargs – Hetal Thaker Jan 11 '23 at 05:14
  • @wim You are right in that regard. If I passed it as `super().__init__(**kwargs)` in class A, then class B would have obtained those parameters. However, this would imply I know and can guarantee that the `__init__` of `object` will not receive any kwargs. This won't be the case if I also instantiated another class directly off A as: d=A(a='x', b='b'). (This would assert). I've updated the question to add more context to the problem. – Anshul Jan 11 '23 at 06:15
  • 2
    Yes, if you pass arbitrary arguments to unknown parents, your class must be designed as a *mixin*, not as an optional stand-alone class *or* mixin. Typically in a class hierarchy, each class would define concrete parameters and only pass the remainders through as kwargs, or at least ensure that used kwargs are removed before passing remainders through to the parent; then all kwargs should be used up and `object` will receive zero kwargs, as it should. – deceze Jan 11 '23 at 06:20
  • @HetalThaker The answer in the link referred to makes sense. I would also like to understand the general design pattern. Anything remaining inside kwargs by the time it reaches the `object` constructor will cause trouble, and we cannot guarantee (in my code) which class the user instantiates and what data they pass. For example, I cannot pop unsupported keywords in `class A`, since they might be used by subsequent `super`. Maybe I should have a BaseClass which handles this case and pops everything before invoking its `super()` and have all other classes inherit from it? – Anshul Jan 11 '23 at 06:21
  • 2
    You design your classes to be used a certain way. If they're used incorrectly, it's fine if that produces an error. You just need to use your classes the way you designed them, or clearly document how they're supposed to be used if you're not going to be the user. Then there shouldn't be much fretting about "maybe another classes uses the argument" or "user instantiates the wrong class". – deceze Jan 11 '23 at 07:40
  • 1
    @Anshul https://stackoverflow.com/q/52272811/674039 might be helpful / duplicate – wim Jan 11 '23 at 19:31
  • @wim Yes, the answer there is very helpful in understanding the overall design process. What deceze has suggested is also, by-and-large similar. So, I'll summarize his key points and close this question. Many thanks! – Anshul Jan 13 '23 at 04:02

1 Answers1

1

As suggested by @deceze and also pointed out in the discussion shared by @wim in the comments to the question, here's a summary (verbatim):

if you pass arbitrary arguments to unknown parents, your class must be designed as a mixin, not as an optional stand-alone class or mixin. Typically in a class hierarchy, each class would define concrete parameters and only pass the remainders through as kwargs, or at least ensure that used kwargs are removed before passing remainders through to the parent; then all kwargs should be used up and object will receive zero kwargs, as it should.

You design your classes to be used a certain way. If they're used incorrectly, it's fine if that produces an error. You just need to use your classes the way you designed them, or clearly document how they're supposed to be used if you're not going to be the user. Then there shouldn't be much fretting about "maybe another classes uses the argument" or "user instantiates the wrong class".

Summary: Do not overgeneralize the classes or try to make them too flexible for the just-in-case scenario that someone later would be using them. In such cases, proper documentation for the person expanding on the classes should be in place.

Anshul
  • 746
  • 9
  • 22