Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What happens if define a class without __init__ method?

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.

like image 325
Eastsun Avatar asked Apr 23 '18 12:04

Eastsun


2 Answers

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

like image 116
MisterMiyagi Avatar answered Sep 24 '22 18:09

MisterMiyagi


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
like image 41
Aran-Fey Avatar answered Sep 22 '22 18:09

Aran-Fey