9

Suppose that I have defined four classes as following:

(The code has been tested on Python 3.6.5. However, I expected it should also works on Python 2.7.x with from __future__ import print_function)

In [1]: class A(object):
   ...:     pass
   ...: 
   ...: class B(object):
   ...:     def __init__(self, value):
   ...:         print('B(value=%s)' % value)
   ...: 
   ...: class C(A):
   ...:     def __init__(self, value):
   ...:         print('C(value=%s)' % value)
   ...:         super(C, self).__init__(value)
   ...: 
   ...: class D(A, B):
   ...:     def __init__(self, value):
   ...:         print('D(value=%s)' % value)
   ...:         super(D, self).__init__(value)
   ...:         

In [2]: C.mro()
Out[2]: [__main__.C, __main__.A, object]

In [3]: D.mro()
Out[3]: [__main__.D, __main__.A, __main__.B, object]

Note two things:

  1. class A without __init__ method;

  2. Both C and D have the same successor A according the mro information.

So I suppose that both super(C, self).__init__(value) and super(D, self).__init__(value) will trigger the __init__ method defined in A.

However, the following result confused me very much!

In [4]: C(0)
C(value=0)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-75d9b7f7d447> in <module>()
----> 1 C(0)

<ipython-input-1-5252938615c6> in __init__(self, value)
      9     def __init__(self, value):
     10         print('C(value=%s)' % value)
---> 11         super(C, self).__init__(value)
     12 
     13 class D(A, B):

TypeError: object.__init__() takes no parameters

In [5]: D(1)
D(value=1)
B(value=1)
Out[5]: <__main__.D at 0x2a6e8755b70

We can see that class D have initialization succeed, while class C failed.

What make the different behavior between class C and class D?

EDIT I don't think my question is about the mro (actually I known the mro about python more or less), my question is about the different behavior of __init___ method.

EDIT2 I'm silly, it's about mro.

Thanks for your help.

Eastsun
  • 18,526
  • 6
  • 57
  • 81
  • Possible duplicate of [How does Python's super() work with multiple inheritance?](https://stackoverflow.com/questions/3277367/how-does-pythons-super-work-with-multiple-inheritance) – Yassine Faris Apr 23 '18 at 12:11
  • 2
    For D it work because when super can't find init in parent class A it try to look for init in class B where init exist – Yassine Faris Apr 23 '18 at 12:13

2 Answers2

9
  1. Class A has no separate __init__ method - it is looked up via A `s own mro.

    >>> A.__init__ is object.__init__
    True
    
  2. Class B has a separate __init__ method - it does not require traversing the mro.

    >>> B.__init__ is object.__init__
    False
    

Note that the difference is having and inheriting __init__. Just because A.__init__ can be provided does not mean A has an __init__ method by itself.

  1. When C looks up its super().__init__, the following is tried:

    • C.__init__ is skipped
    • A.__init__ does not exist
    • object.__init__ exists and is called
  2. When D looks up its super().__init__, the following is tried:

    • D.__init__ is skipped
    • A.__init__ does not exist
    • B.__init__ exists and is called
    • object.__init__ is never looked up

This difference is a major point to using super instead of an explicit base class. It allows inserting specialised classes into a hierarchy (example).

MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
  • 1
    `C.__init__` does not exist? Looks like it was defined. – Qiang Xu Mar 03 '20 at 00:37
  • 1
    @QiangXu Thanks for pointing this out. I have adjusted the answer accordingly. – MisterMiyagi Mar 03 '20 at 08:29
  • `super(C, self).__init__` was called from inside `C__init__`, right? If that is the case, then `C.__init__` was not skipped, was it? Please correct me if I am wrong on this. – Qiang Xu Mar 03 '20 at 15:21
  • 1
    @QiangXu It is skipped *in the lookup of* C's ``super().__init__``. ``super`` starts its search in the mro *after* the current method's class. – MisterMiyagi Mar 03 '20 at 15:25
  • `super(C, self).__init__` was invoked by `C.__init__`, wasn't it? I am a bit confused... – Qiang Xu Mar 03 '20 at 15:35
  • 1
    @QiangXu Yes, ``C.__init__`` invokes ``super(C, self).__init__``. But when ``super(C, self).__init__`` looks for ``__init__`` in the mro, it skips ``C.__init__``. Otherwise, ``C.__init__`` would keep calling itself infinitely. – MisterMiyagi Mar 03 '20 at 15:49
3

The reason for this behavior is the way attribute lookup works in python. When you access A.__init__, python internally traverses A's MRO until it finds a class that defines an __init__ attribute. Inherited attributes are disregarded during this lookup.

When you call super(C, self).__init__(value) in C.__init__, python traverses the MRO [A, object] until it finds an __init__ attribute. The important thing is that A does not define an __init__ attribute, so the lookup goes past A to object and returns object.__init__.

The same thing happens in D.__init__, except in this case the MRO being traversed is [A, B, object]. Again, A doesn't define an __init__, so the lookup continues and returns B.__init__.


As an experiment, you can change the definition of A to define an __init__ like so:

class A(object):
    __init__ = object.__init__

And you'll notice that instantiating D now throws the same error as C:

>>> D(3)
D(value=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "untitled.py", line 17, in __init__
    super(D, self).__init__(value)
TypeError: object.__init__() takes no parameters
Aran-Fey
  • 39,665
  • 11
  • 104
  • 149