Drew Ackerman
Drew Ackerman

Reputation: 341

Using super() in Python, I do not understand this last __init__ call

I have three classes as follows:

class Page(object):
    def __init__(self, Obj_a, Obj_b):
        super().__init__(Obj_a, Obj_b)

class Report(object):
    def __init__(self, Obj_a, Obj_b):
        super().__init__()

class ReportingPage(Page,Report):
    def __init__(self, Obj_a, Obj_b):
        super().__init__(Obj_a, Obj_b)

I instantiate a ReportingPage object. To do this Python crawls up the MRO:

  1. The Page object is called first, as it's ordered first in the inheritance list for ReportingPage, where it calls its own __init__ method.

  2. Then it does the same for Report.

Two things I don't understand:

  1. Why I must pass in arguments to the super.__init__ in Page, when Page is just going to call __init__ on what it inherits from, object.

  2. Why I don't have to do the same thing for Report.

Upvotes: 4

Views: 592

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1122342

super() looks at the MRO of the current instance. It doesn't matter here that the current class inherits only from object.

The MRO of ReportingPage puts Report between Page and object:

>>> ReportingPage.__mro__
(<class '__main__.ReportingPage'>, <class '__main__.Page'>, <class '__main__.Report'>, <class 'object'>)

So when you call super() in Page.__init__(), the next class in the MRO is Report, and you end up calling the Report.__init__ method.

You need to make your classes more cooperative; you could use keyword arguments and a catch-all **kwargs argument to do so:

class Page(object):
    def __init__(self, pagenum, **kwargs):
        self.pagenum = pagenum
        super().__init__(**kwargs)

class Report(object):
    def __init__(self, title, **kwargs):
        self.title = title
        super().__init__(**kwargs)

class ReportingPage(Page, Report):
    def __init__(self, footer=None, **kwargs):
        self.footer = footer
        super().__init__(**kwargs)

Each method passes along the remaining keyword arguments here, to the next __init__ in the MRO, and in the end you'll have an empty dictionary to pass to object.__init__(). If you add in a print(kwargs) wrapper to each __init__ method, you can see that kwargs becomes smaller as fewer values are passed on to the next call.

>>> def print_wrapper(name, f):
...     def wrapper(*args, **kwargs):
...         print(name, '->', kwargs)
...         return f(*args, **kwargs)
...     return wrapper
...
>>> for cls in ReportingPage.__mro__[:-1]:  # all except object
...     cls.__init__ = print_wrapper(cls.__name__, cls.__init__)
...
>>> ReportingPage(title='Watching Paint Dry II: The Second Coat', pagenum=42)
ReportingPage -> {'title': 'Watching Paint Dry II: The Second Coat', 'pagenum': 42}
Page -> {'title': 'Watching Paint Dry II: The Second Coat', 'pagenum': 42}
Report -> {'title': 'Watching Paint Dry II: The Second Coat'}
<__main__.ReportingPage object at 0x109e3c1d0>

Only title remains, which Report.__init__() consumes, so an empty kwargs dictionary is passed to object.__init__()

You may be interested in Raymond Hettinger's super considered super, including his PyCon 2015 presentation.

Upvotes: 6

Related Questions