Search code examples
pythonpython-3.xmultiple-inheritance

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.


Solution

  • 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.