Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Python

OOP in Python - Part 2

4.91/5 (9 votes)
13 Dec 2014CPOL6 min read 24.4K  
Object Oriented Programming in Python - part 2 (Inheritance)

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:

  1. Model - a relationship (not true in every programming language, there are cases when only the implementation is shared)

  2. Reuse Code – helps developers to respect the DRY principle and reuse the existing implementation and logic in code

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

Python
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

    #
    # METHODS
    #

    def eat(self, food):
        print("{} is eating {}.".format(self.name, food))
    
    #
    # PROPERTIES
    #
    
    @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.

Python
class Snake(Animal):
    __temperature = 28    
    
    def __init__(self, name, temperature):
        super().__init__(name, 2, True, 0)
        self.temperature = temperature 

    #
    # METHODS
    #

    def eat(self, food):
        if food == "meat":
            super().eat(food)
        else:
            raise ValueError    

    #
    # PROPERTIES
    #

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

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

Python
#
# will be discussed later why only the constructor of Phone class was invoked
#
>>> 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):

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

Python
>>> 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:

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

Python
>>> 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
>>>

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)