Let us get back to the fundamental philosophical idea with which we began the study of object oriented programming.
Unity in diversity.
The idea of a class represents the unity, the idea of objects represent the diversity. But this diversity that we see around us is not chaotic and unordered. On the contrary, there is an organized hierarchy that we see almost everywhere around us. Consider the following image:
We humans take up different roles. Some of us are students, others are working professionals. The beauty of this program is that we have working professionals who are at the same time students. Getting back to the point, we see that there is a hierarchy. All college students are students. All students are humans. In the other branch of this directed graph, all full-stack developers are software developers, all software developers are working professionals. The basic idea behind the concept of inheritance is this:
Classes that are lower in the hierarchy inherit features or attributes from their ancestors.
There are certain features of a class higher up in the hierarchy that can be inherited by classes lower in the hierarchy. For example, all working professionals draw a salary every month. All software developers also draw some monthly salary, because they belong to the class of working professionals. So, salary is an attribute that is common to all classes that are derived from the class of working professionals. Needless to say, a full stack developer inherits this attribute of salary from his ancestors in the graph.
We shall take up a concrete example and see inheritance in action.
By now you would have worked on plenty of assignments across multiple courses. Each assignment is a collection of questions. Questions come in different types, some are NAT, some MCQ. So, a NAT question is not of the same type as a MCQ question. Yet, both are questions. So, we see that there is a hierarchy of relationships here:
Parents always come first in the hierarchy. So, let us first define a class to represent a question:
1class Question:
2 def __init__(self, statement, marks):
3 self.statement = statement
4 self.marks = marks
5
6 def print_question(self):
7 print(self.statement)
8
9 def update_marks(self, marks):
10 self.marks = marks
Note that we have only retained those elements as attributes that are common to all questions, irrespective of the type:
The next step is to define two new classes for the children of Question
, one for MCQ and the other for NAT. It is here that we make use of the relationship that we just diagrammed:
xxxxxxxxxx
71class NAT(Question):
2 def __init__(self, statement, marks, answer):
3 super().__init__(statement, marks)
4 self.answer = answer
5
6 def update_answer(self, answer):
7 self.answer = answer
NAT
is also a Question
, but a specialized question. Specifically, it has an additional feature, answer
, and a new method, update_answer
. But all the other attributes and methods of Question
are inherited by it, since NAT
is just another Question
.
We say that NAT
is derived from Question
. Question
becomes the parent-class or base-class , and NAT
is a child-class or derived-class.
In Python, the syntax used to make this dependency explicit is as follows:
xxxxxxxxxx
81class Derived(Base):
2 def __init__(self, ...):
3 pass
4
5#### OR ####
6class Child(Parent):
7 def __init__(self, ...):
8 ...
In our specific example, we have:
xxxxxxxxxx
31class NAT(Question):
2 def __init__(self, ...):
3 pass
Note that something interesting happens within the constructor of the derived class:
xxxxxxxxxx
71class NAT(Question):
2 def __init__(self, statement, marks, answer):
3 super().__init__(statement, marks)
4 self.answer = answer
5
6 def update_answer(self, answer):
7 self.answer = answer
The super()
function points to the parent class, in this case Question
. So, in line-3, we are effectively calling the constructor of the parent class. If we need to update the marks, we can just invoke the method update_marks
that is inherited from Question
:
xxxxxxxxxx
31q_nat = NAT('What is 1 + 1?', 1, 2)
2q_nat.update_marks(4)
3print(q_nat.marks)
Let us now turn our attention to methods. Pasting the parent-class here for easy reference:
xxxxxxxxxx
101class Question:
2 def __init__(self, statement, marks):
3 self.statement = statement
4 self.marks = marks
5
6 def print_question(self):
7 print(self.statement)
8
9 def update_marks(self, marks):
10 self.marks = marks
Sometimes we may want to modify the behaviour of existing methods in the parent class. For example, take the case of a MCQ question. For questions of this type, the statement of a problem is incomplete without the options. The print_question
method in the parent class just prints the statement, but it makes more sense to print the options as well for a MCQ question. So, we want the print_question
to behave differently. Though we have inherited this method from the parent class, we can override the behaviour of the method in the following way:
xxxxxxxxxx
121class MCQ(Question):
2 def __init__(self, statement, marks, ops, c_ops):
3 super().__init__(statement, marks)
4 self.ops = ops # list of all options
5 self.c_ops = c_ops # list of correct options
6
7 def print_question(self):
8 super().print_question()
9 # Assume there are only four options
10 op_index = ['(a)', '(b)', '(c)', '(d)']
11 for i in range(4):
12 print(op_index[i], self.ops[i])
Note that the parent class Question
already prints the statement. So, we piggy-back on this behaviour using the super()
function in line-8. In addition, we also print the options. Let us create a MCQ
question object and see how it all works:
xxxxxxxxxx
51q_mcq = MCQ('What is the capital of India?',
2 2,
3 ['Chennai', 'Mumbai', 'Kolkota', 'New Delhi'],
4 ['New Delhi'])
5q_mcq.print_question()
This returns the output:
xxxxxxxxxx
51What is the capital of India?
2(a) Chennai
3(b) Mumbai
4(c) Kolkota
5(d) New Delhi