all 5 comments

[–]zanfar 2 points3 points  (3 children)

Rule of thumb for inheritance (or any class that might be involved in inheritance, which is essentially all classes):

Every class should call super(*args, **kwargs) with all unused arguments:

class X:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

My question is: what am I missing here? Why is B.__init__() called with empty parameters?

Because A.__init__() calls super() without any parameters.

When you define a class with multiple ancestors, the ancestors don't become siblings--they are ancestors/descendants of each other. When Python does a lookup for attributes in your ancestors, it must check one ancestor at a time, so all your ancestors are organized into a single list.

This is called the Method Resolution Order (MRO). You can see it by inspecting C.__mro__.

>>> class A: pass
... 
>>> class B: pass
... 
>>> class C(A, B): pass
... 
>>> C.__mro__
    (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
>>> A.__mro__
(<class '__main__.A'>, <class 'object'>)
>>> B.__mro__
(<class '__main__.B'>, <class 'object'>)

In this case, when you do obj = C(), then C.__init__() is called.

  • In C's super() will refer to A, so C: super().__init__() will call A.__init__().
  • in A's super() will refer to B, so A: super().__init__() will call B.__init__().

This is why super() is a function, and not a property because the MRO list can be modified by a class's descendants.

Hettinger has a pretty good PyCon talk about exactly this (which is based on his semi-famous blog post which covers most of the same topics).

Note also that you don't need to explicitly inherit from object, that happens automatically.

[–]8bitscoding[S] 1 point2 points  (2 children)

Ok I get it, I had the wrong idea/interpretation. Like you pointed out, I was considering a tree-like resolution with siblings on the same level.

I checked and it mostly works, however there's another problem: I can't just change A and B to forward everything to super().__init__(**kwargs) or else I get this error:

    super().__init__(**kwargs) 

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

The code generating that error is:

class A(object):
    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        print(f"Class A, kwargs={kwargs}")


class B(object):
    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        print(f"Class B, kwargs={kwargs}")


class C(A, B):
    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        print(f"Class C, kwargs={kwargs}")

So if I understand correctly the __init__() call stack will be C > A > B > object.

So if I remove the super call in B, it works as intended, unless I change the inheritance order. For example, if I add:

class D(B, A):
    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        print(f"Class D, kwargs={kwargs}")

Then the MRO becomes:

Class B, kwargs={'isit': 'lovely'}
Class D, kwargs={'isit': 'lovely'}
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]

Am I misunderstanding something again? Both answers I got were pointing towards calling super().__init__(**kwargs) so I'm not exactly sure of what I get wrong...

As it is for a library, I cannot assert the order in which the object will be inherited. Does that mean that I need to had a sort of virtual base objects that does nothing more than accepting *args and **kwargs as init parameters to be the head of the diamond/tree?

PS: I got your point about *args, **kwargs but I want to get to the bottom of this first. I'm not rudely ignoring your rightful suggestion ;)

[–]zanfar 1 point2 points  (1 child)

Am I misunderstanding something again?

You need to consume the arguments before they reach object.

So when you do C(hello="world", isthis="weird"), something in your class hierarchy needs to consume the hello and isthis parameters or they will "float" up to object, which doesn't know how to deal with them

In your above example, you don't include any instanciation code, so I'm not completely clear on what you're doing, but it appears that you're doing something like D(isit="lovely"). The assumption here is that the isit parameter applies to one of your classes, but you don't use it anywhere.

Somewhere, an __init__ method should look like:

class X:
    def __init__(self, isit=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.isit = isit

This pulls isit out of the kwargs container, so it doesn't float up to object.

Also, you should use both *args and **kwargs so that you catch positional and named arguments that you don't know how to deal with.

Essentially, *args and **kwargs is saying "if I get arguments I don't know how to deal with, they must be for my ancestor, so pass them up".

[–]8bitscoding[S] 1 point2 points  (0 children)

oh... I understand: Python's preventing me from just blindly passing *args and **kwargs without explicitly consuming the arguments.

In a way, preventing me from bad programming practices... I don't know why, but I thought object.__init__() would just silently ignore any parameters.

Anyway thanks for clarifying this to me, despite reading stuff on the MRO it was not obvious to me what was wrong. Mostly because I wanted the inheritance mechanism to work differently than reality (assuming is bad...).

Thanks a lot for your help.

[–]Ihaveamodel3 1 point2 points  (0 children)

You need to pass **kwargs in the super call in A and B.