In the first part of this article, I introduced the 3 pillars of object oriented programming. I covered Encapsulation, the next big topic is Inheritance.
What is Inheritance in OOP?
Inheritance is a concept in object oriented programming which helps programmers to:
-
Model - a relationship (not true in every programming language, there are cases when only the implementation is shared)
-
Reuse Code – helps developers to respect the DRY principle and reuse the existing implementation and logic in code
-
Extend functionality – there are some cases when the source code of the used classes cannot be changed (others are using it or it’s not public
or simply it’s not allowed); in that case extending the functionality of a class can be done by applying inheritance.
Python supports single and multiple inheritance. There are major differences on how to implement inheritance and how it works in Python 2.x and 3.x. All the examples that I’ll give are for Python 3.x.
Single Inheritance in Python
In Python, inheritance can be done via the class MySubClass(MyBaseClass)
syntax. Basically, after the new class name in parenthesis, I specify the superclass name.
I’ll stick with the traditional example of Animal
being the base (or so called super) class and the different species of animals, called the subclasses.
class Animal:
__name = None
__age = 0
__is_hungry = False
__nr_of_legs = 0
def __init__(self, name, age, is_hungry, nr_of_legs):
self.name = name
self.age = age
self.is_hungry = is_hungry
self.nr_of_legs = nr_of_legs
def eat(self, food):
print("{} is eating {}.".format(self.name, food))
@property
def name(self):
return self.__name
@name.setter
def name(self,new_name):
self.__name = new_name
@property
def age(self):
return self.__age
@age.setter
def age(self,new_age):
self.__age = new_age
@property
def is_hungry(self):
return self.__is_hungry
@is_hungry.setter
def is_hungry(self,new_is_hungry):
self.__is_hungry = new_is_hungry
@property
def nr_of_legs(self):
return self.__nr_of_legs
@nr_of_legs.setter
def nr_of_legs(self,new_nr_of_legs):
self.__nr_of_legs = new_nr_of_legs
In the Animal
class, I defined 4 private
members (__name
, __age
, __is_hungry
, __nr_of_legs
). For all 4 members, I created properties using the decorator @property
creation presented in the first part of the OOP in Python article. I created a constructor with 4 parameters (not counting the self
parameter) which uses the properties to set the values for the private
members. Besides the 4 properties, I created a method called eat(self, food)
, which prints out X is eating Y
, where X
is the name of the animal
and Y
is the food passed in as parameter. The Animal
class serves as base class for the Snake
class.
class Snake(Animal):
__temperature = 28
def __init__(self, name, temperature):
super().__init__(name, 2, True, 0)
self.temperature = temperature
def eat(self, food):
if food == "meat":
super().eat(food)
else:
raise ValueError
@property
def temperature(self):
return self.__temperature
@temperature.setter
def temperature(self,new_temperature):
if new_temperature < 10 or new_temperature > 40:
raise ValueError
self.__temperature = new_temperature
The constructor of the Snake
class takes 2 arguments, the name of the snake
and its temperature
. For the __temperature private
member, I created a property, so I am using that in the constructor to store the value passed to the constructor. In the constructor, first I call the base class’ construct using the super()
keyword (there are other methods to invoke the parent class’ constructor but in Python 3.x, this is the recommended way). When invoking the base class’ constructor, I passed in some predefined values, like nr_of_legs
zero, since snakes don’t have legs and is_hungry
as True
, because snakes tend to be “hungrier” than other animals.
As in other programming languages, I can override methods in Python too. I’ve overwritten the eat
method and I added extra logic. In case the food which is given to the snake
is not meat, then I’m raising a ValueError, otherwise I’m invoking the eat
method which is defined in the base class (Animal
).
Multiple Inheritance in Python
Python gives us the possibility to derive from multiple base classes, this is called multiple inheritance. This can be useful if there are classes with different functionality, but these functionalities can be combined and used together.
In Python, the syntax for inheriting from multiple classes is very easy, all that needs to be done is to enumerate the base classes within parenthesis after the new class’ name, for example: class MySubClass(MyBaseClass1, MyBaseClass2, MyBaseClass3)
.
class Phone:
def __init__(self):
print("Phone constructor invoked.")
def call_number(self, phone_number):
print("Calling number {}".format(phone_number))
class Computer:
def __init__(self):
print("Computer constructor invoked.")
def install_software(self, software):
print("Computer installing the {} software".format(software))
class SmartPhone(Phone,Computer):
def __init__(self):
super().__init__()
I defined 3 classes (Phone
, Computer
, SmartPhone
), 2 are base classes (Phone
and Computer
) and one derived class SmartPhone
. Logically, this seams to be correct, a smartphone can make calls and can install software, so the SmartPhone
class can call_number
(inherited from Phone
class) and can install_software
(inherited from the Computer
class).
>>> my_iphone = SmartPhone()
Phone constructor invoked.
>>> my_iphone.call_number("123456789")
Calling number 123456789
>>> my_iphone.install_software("python")
Computer installing the python software
If I look at the constructor of the SmartPhone
class, it's pretty simple, it invokes the base class’s constructor. That’s correct, it invokes a constructor, the question is from which base class? As it can be seen on the code, it invokes only the constructor of the Phone
class. The question is why?
The explanation is not simple and is related to Python’s MRO (also known as Method Resolution Order).
Method Resolution Order in Python
The Method Resolution Order (MRO) is a set of rules which help to define and determine the linearization of a class. Linearization (also called precedence list) is the list of ancestors (classes) of a class, ordered from the nearest to the farest. The MRO is important only for programming languages where multiple inheritance is allowed. The MRO helps programming languages to deal with the Diamond Problem. In Python 2.3, there was a radical change in the rules which helped to define a more concrete MRO of classes (this version uses C3 linearization), Michele Simionato wrote a very good post about the method and algorithm, the post is not short, but contains a lot of examples and detailed explanation of the algorithm. Guido van Rossum wrote an exhaustive article about Python’s MRO too. Perl programming language also uses the C3 Linearization algorithm to define the MRO of classes.
To give response to my initial questions: Why does super().__init__()
only invoke the constructor of the Phone
class? That happens, because, the method resolution order of the base classes, when invoking the super().__init__()
resolves to the Phone
class (first of the parent classes in the MRO). The __init__()
method of the other base classes appearing in the MRO are not invoked. The MRO affects how the constructors of base classes are invoked. When using multiple inheritance, I, as a developer, have to ensure my base classes are initialized properly. I updated the code of the SmartPhone
class to ensure both Computer
and Phone
classes are initialized (plus I added the object as base class for Phone
and Computer
classes to ensure you get the same MRO if running in python 2.x):
class Phone(object):
def __init__(self):
print("Phone constructor invoked.")
def call_number(self, phone_number):
print("Calling number {}".format(phone_number))
class Computer(object):
def __init__(self):
print("Computer constructor invoked.")
def install_software(self, software):
print("Computer installing the {} software".format(software))
class SmartPhone(Phone, Computer):
def __init__(self):
Phone.__init__(self)
Computer.__init__(self)
When creating a new SmartPhone
class, now both constructors are executed:
Python 3.2.3 (default, Feb 27 2014, 21:31:18)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from oop_multi import SmartPhone
>>> s = SmartPhone()
Phone constructor invoked.
Computer constructor invoked.
>>>
The MRO of a class can be displayed in python using the mro()
method or using the __mro__
attribute of the class.
>>> SmartPhone.mro()
[<class 'SmartPhone'>, <class 'Phone'>, <class 'Computer'>, <class 'object'>]
>>> SmartPhone.__mro__
(<class 'SmartPhone'>, <class 'Phone'>, <class 'Computer'>, <class 'object'>)
The first item in the MRO is always the class we invoked the mro() method on. After the actual class, there is the Phone
class, followed by the Computer
and lastly, the base class for all the three is specified, object
.
Defining the MRO in case of big class hierarchies can be difficult and in some cases it cannot be done. The python interpreter will throw a TypeError
in case there are cross references between base classes, for example:
class Phone(object):
def __init__(self):
print("Phone constructor invoked.")
def call_number(self, phone_number):
print("Calling number {}".format(phone_number))
class Computer(object):
def __init__(self):
print("Computer constructor invoked.")
def install_software(self, software):
print("Computer installing the {} software".format(software))
class SmartPhone(Phone,Computer):
def __init__(self):
Phone.__init__(self)
Computer.__init__(self)
class Tablet(Computer,Phone):
def __init__(self):
Computer.__init__(self)
Phone.__init__(self)
class Phablet(SmartPhone,Tablet):
def __init__(self):
SmartPhone.__init__(self)
Tablet.__init__(self)
Above I created two new classes, Tablet
which derives from Computer
and Phone
(notice the change in order compared to SmartPhone
where I derived from Phone
and Computer
) and Phablet
which derives from SmartPhone
and Tablet
. Since SmartPhone
and Tablet
classes have cross referenced their base classes (Phone
and Computer
), the python interpreter will raise a TypeError
since the C3 Linearization algorithm cannot decide what is the exact order of ancestors.
>>> from mro_type_error import Phablet
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "mro_type_error.py", line 30, in <module>
class Phablet(SmartPhone, Tablet):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases Phone, Computer
>>>