# Inheritance¶

If we're writing a new class that needs similar methods as another class, we could copy/paste those. A better way is inheritance. class Child(Parent): means that the new class named Child inherits methods from the previous class named Parent.

In [1]:
class Parent:
def f(self):
print("Parent: f")

def g(self):
print("Parent: g")

def h(self):
print("Parent: h")

class Child(Parent):
def h(self):
print("Child: h")

youngster = Child()
youngster.f()
youngster.g()
youngster.h()

Parent: f
Parent: g
Child: h


In your mental model, you should imagine the f and g methods being copied to the Child class, like this:

In [2]:
class Child(Parent):
def f(self):
print("Parent: f")

def g(self):
print("Parent: g")

def h(self):
print("Child: h")

youngster = Child()
youngster.f()
youngster.g()
youngster.h()

Parent: f
Parent: g
Child: h


The h method doesn't get "copied" from Parent because Child already has a method called that. The technical way to describe this situation is to say that Child overrides the h method of the base class Parent.

Every class we create has a parent. If we don't specify, its parent is a class named object (yes, it is confusing that a class is named object, since we create objects from classes; in Python code, object isn't an object, it's a class).

This means we always inherit some special methods, like __str__.

In [3]:
class C:
pass

obj = C()

obj.__str__() # inherited from the class named "object"

Out[3]:
'<__main__.C object at 0x7f908edd48d0>'

This is very convenient; otherwise, we wouldn't be able to print obj because doing so implicitly calls __str__:

In [4]:
print(obj)

<__main__.C object at 0x7f908edd48d0>


## Multiple Inheritance¶

Every class we create has at least one parent (object at a minimum), but we're allowed to have 2, 3, or more parents. This capability is called multiple inheritance -- it's a capability that many other programming languages lack.

In [5]:
class Parent1:
def f(self):
print("Parent1: f")

class Parent2:
def f(self):
print("Parent2: f")

def g(self):
print("Parent2: g")

class Parent3:
def g(self):
print("Parent3: g")

class Child(Parent1, Parent2, Parent3):
pass

youngster = Child()


This is a confusing situation! For f, which method do we inherit? Both Parent1 and Parent2 have an f method, after all. Similarly, both Parent2 and Parent3 have a g method.

Fortunately, the Child.__mro__ tuple will tell us Python's preferences in terms of where to find methods.

In [6]:
Child.__mro__

Out[6]:
(__main__.Child, __main__.Parent1, __main__.Parent2, __main__.Parent3, object)
In [7]:
for cls in Child.__mro__:
print(cls)

<class '__main__.Child'>
<class '__main__.Parent1'>
<class '__main__.Parent2'>
<class '__main__.Parent3'>
<class 'object'>


This means we'll use the f method from Parent1 instead of the f method from Parent2, as Parent1 is higher priority (earlier in the tuple). Similarly, we'll use g from Parent2 instead of Parent3. This means we can predict what the following will print:

In [8]:
youngster.f()
youngster.g()

Parent1: f
Parent2: g


__mro__ stands for "Method Resolution Order", and it's a special attribute that every class has. Don't confuse this with the special methods that we've called on our objects. Contrast youngster.__str__() (parentheses are necessary for method calls, special or not) with Child.__mro__ (just an attribute, so no parentheses; also, it's a class attribute, in contrast to object attributes we more frequently encounter).

Python has some rules for determining the method-resolution order:

1. prefer closer: if both a parent class and grandparent class have the same method, take the version from the parent (which is closer to the child)
2. prefer left: in class SomeClass(Parent1, Parent2, Parent3), Parent1 appears first (most to the left) in the list, so that is the highest priority; similarly, Parent2 is higher priority than Parent3
3. prefer descendants: if both A and B are ancestors of our class C via separate paths, and A is an ascestor of B, then B will have priority over A

Situations often arise where the above rules contradict each other. The above rules are from weakest (1) to strongest (3); this allows us to resolve contradictions. Let's see an example:

In [9]:
class Top:
def f(self):
print("Top")

class Level_1A(Top):
pass

class Level_1B(Top):
def f(self):
print("Level_1B")

class Level_2(Level_1B):
pass

class Level_3(Level_2):
pass

class Bottom(Level_1A, Level_3):
pass

b = Bottom()
b.f()

Level_1B


Here, Bottom could inherit f from either Top or Level_1B. Level_1B is a descendent of Top, so the Level_1B version is chosen, according to rule 3.

This shows the strength of rule 3. Rule 1 would have preferred taking f from Top, as Bottom => Level_1A => Top is shorter than Bottom => Level_3 => Level_2 => Level_1B => Top. Rule 2 would have also preferred taking f from Top, as the Bottom => Level_1A => Top path is follows the leftmost parents up. Yet Rule 3 wins and f is taken from Level_1B.

In [ ]: