Inheritance Notes
Inheritance Notes
PGStudent
is a Student. The state and behaviour of Student is inherited by PGStudent. PGStudent has additional
state and additional methods.
def getName(self):
return self.name
def getFee(self):
return self.fee
def getID(self):
return self.id
def setPhone(self,ph):
self.phone = ph
def getPhone(self):
return self.phone
def __str__(self):
return "ID:"+self.id+" Name:"+self.name+" Fee:"+str(self.fee)
PGStudent inherits the state and behaviour from Student class. It also has additional state GateScore. It
also has additional method ( getGateScore()). When a print method is called on Student object, it prints
id, name, and fee. When a print method is called on PGStudent, it should print the GATESCORE along
with the id, name, and fee. The print method inherited is no longer sufficient. We need to override the
inherited method with new behaviour.
In [5]: #%% Single Inheritance Example: Base class Student
class Student:
def __init__(self,idNo,name,fee):
self.id=idNo
self.name=name
self.fee=fee
def getName(self):
return self.name
def getFee(self):
return self.fee
def getID(self):
return self.id
def setPhone(self,ph):
self.phone = ph
def getPhone(self):
return self.phone
def __str__(self):
return "ID:"+self.id+" Name:"+self.name+" Fee:"+str(self.fee)
# Subclass/Childclass PGStudent.
#has new method, overriding one method.
# Subclass has one new method, overriding one method.
class PGStudent(Student):
def __init__(self,idNo,name,fee,gs):
Student.__init__(self, idNo, name, fee)
self.gs = gs
def getGateScore(self):
return self.gs
def __str__(self):
msg = Student.__str__(self)+" Gate Score:"+str(self.gs)
# msg = "Using super() :"+super().print()+" Gate Score:"+str(self.gs)
return msg
In [6]: #Test
s1 = Student("001","Rahul",50000)
print(s1)
s2 = PGStudent("002","Rohan",40000,99)
print("Name:",s2.getName()) # calling inherited method
print("Gate Score:",s2.getGateScore()) # calling new method at subclass.
print(s2) # calling overridden method
Abstract Class Example: Python on its own doesn't provide abstract classes. Yet, Python comes with a
module which provides the infrastructure for defining Abstract Base Classes (ABCs).
In [1]: from abc import ABC, abstractmethod
import math
class Polygon(ABC):
def noofsides(self):
return self.n
def perimeter(self):
sum=0
for i in range(self.n):
sum += self.sideLengths[i]
return sum
class Triangle(Polygon):
# define with 3 sides
def __init__(self, s1,s2,s3):
Polygon.__init__(self, [s1,s2,s3])
edge1 = self.sideLengths[0]
edge2 = self.sideLengths[1]
edge3 = self.sideLengths[2]
s = (edge1+edge2+edge3)/2.0
return math.sqrt(s*(s-edge1)*(s-edge2)*(s-edge3))
class Rectangle(Polygon):
# define with 2 side lengths
def __init__(self, l, b):
Polygon.__init__(self, [l,b,l,b])
class Square(Polygon):
# define with 1 side length
def __init__(self, s):
Polygon.__init__(self, [s,s,s,s])
# Driver code
p = Triangle(3,5,6)
print("Sides:",p.noofsides()," Perimeter:",p.perimeter()," Area:",p.area())
p = Rectangle(3,5)
print("Sides:",p.noofsides()," Perimeter:",p.perimeter()," Area:",p.area())
p = Square(5)
print("Sides:",p.noofsides()," Perimeter:",p.perimeter()," Area:",p.area())
# abstract class objects can not be created. Following line gives compilation error
list = [1,2,3]
p=Polygon(list)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Input In [1], in <cell line: 80>()
78 # abstract class objects can not be created. Following line gives compilati
on error
79 list = [1,2,3]
---> 80 p=Polygon(list)
TypeError: Can't instantiate abstract class Polygon with abstract method area
If a class does not mark any method as 'abstractmethod' then its objects can be created.
class P(ABC):
def noofsides(self):
return self.n
p1 = P([1,2,3])
p1.noofsides()
Out[1]: 3
A class can mark any method as 'abstractmethod' even though its implementation is provided. Objects
can not be created for such classes. The only way to use it is, create a concrete subclass and override
the abstract method. If the logic present in the base class is suitable then we can call the base class
implementation with the help of super(). This forces the users to understand the assumptions made in
the base class.
In [3]: from abc import ABC, abstractmethod
import math
class P(ABC):
@abstractmethod
def noofsides(self):
return self.n
p1 = P([1,2,3])
p1.noofsides()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Input In [3], in <cell line: 14>()
10 @abstractmethod
11 def noofsides(self):
12 return self.n
---> 14 p1 = P([1,2,3])
15 p1.noofsides()
class P1(ABC):
@abstractmethod
def noofsides(self):
return self.n
class P2(P1):
def noofsides(self):
return super().noofsides()
p2 = P2([1,2,3])
p2.noofsides()
Out[4]: 3
I'm barking
I am charging...
Charging Completed.
I am walking using my wheels...I'm walking with my legs
Single Inheritance: Same method inherited from the parent class and the method is also
redefined/overridden in the subclass. Result: i) When you call the method on the parent object, the
parent implementation is called. ii) When you call the method on the child object, The overridden
method at the subclass is called.
In [15]: class A1:
def process(self):
print('A1 process()')
class B(A1):
def process(self):
print('B process called')
obj1 = A1()
obj1.process()
obj3 = B()
obj3.process()
A1 process()
B process called
Multiple Inheritance: Same method inherited from more than one parent class and the method is also
redefined/overridden in the subclass. Result: Same behaviour as above.
class A2:
def process(self):
print('A2 process()')
class B(A1,A2):
def process(self):
print('B process called')
obj1 = A1()
obj1.process()
obj2 = A2()
obj2.process()
obj3 = B()
obj3.process()
A1 process()
A2 process()
B process called
Multiple Inheritance: Same method inherited from more than one parent class and the method is not
defined in the subclass. Result: Method resolved based on the inheritance order specified. Experiment
by changing the inheritance order.
In [3]: class A1:
def process(self):
print('A1 process()')
class A2:
def process(self):
print('A2 process()')
class B(A1,A2):
pass
obj3 = B()
obj3.process()
A1 process()
Multiple Inheritance: Same method inherited from more than one parent class and the subclass wants to
specific base class version. Programmer specifies which implementation to be used by specifying the
parent class name. B wants to use the version of A2 instead of A1.
Result: Method resolved based on the parent class specified. Experiment by changing the parent class
name.
class A2:
def process(self):
print('A2 process called...')
class B(A1,A2):
def process(self):
A2.process(self)
obj3 = B()
obj3.process()
A2 process called...
Using super(). The parent class order specified is used for resolving the ambiguity. Result: Method
resolved based on the parent class order specified. Experiment by changing the parent class order.
In [1]: class A1:
def process(self):
print('A1 process called...')
class A2:
def process(self):
print('A2 process called...')
class B(A1,A2):
def process(self):
super().process()
obj3 = B()
obj3.process()
A1 process called...
Example: Diamond problem- class diamond structure (A, B, C, D - see the diagram below) Many
combinations are possible: Case 1: the method specified at A, B, C. But not at D. An object of D invokes
the method. Case 2: the method specified at A, C. But not at B and D. An object of D invokes the
method. Case 3: the method specified at A, B. But not at C and D. An object of D invokes the method.
Python 3 uses C3 linearization for Method Resolution. This order is similar to Topological sort order.
In [6]: #%% Diamond problem- class diamond structure
class A:
def go(self):
return 'I am super class-A'
class B(A):
# pass
def go(self):
return 'I am class B-f'
class C(A):
def go(self):
return 'I am class C-f'
# Scenario 1
class D(B,C):
pass
# Scenario 2
#class D(C,B):
# pass
obj_d = D()
print(obj_d.go())
print("The Method Resolution Order:",D.mro())
I am class B-f
The Method Resolution Order: [<class '__main__.D'>, <class '__main__.B'>, <class '_
_main__.C'>, <class '__main__.A'>, <class 'object'>]
class C(A):
def go(self):
return 'I am class C-f'
# Scenario 1
class D(B,C):
pass
# Scenario 2
#class D(C,B):
# pass
obj_d = D()
print(obj_d.go())
I am class C-f
Difference between using super() vs Parent class name to resolve the ambiguity.
Scenario 1: Explicit calls to base class methods using the class name.
In [5]: #%% Diamond problem- super class logic excuted multiple times.
class A:
def f(self):
print('I am A')
class B(A):
def f(self):
print('Entered ClassB')
A.f(self)
print('Leaving ClassB')
class C(A):
def f(self):
print('Entered ClassC')
A.f(self)
print('Leaving ClassC')
class D(B, C):
def f(self):
print('Entered ClassD')
B.f(self) # case 2
C.f(self) # case 3
print('Leaving ClassD')
obj_d = D()
obj_d.f()
Entered ClassD
Entered ClassB
I am A
Leaving ClassB
Entered ClassC
I am A
Leaving ClassC
Leaving ClassD
The Method Resolution Order: [<class '__main__.D'>, <class '__main__.B'>, <class '_
_main__.C'>, <class '__main__.A'>, <class 'object'>]
Entered ClassD
Entered ClassB
Entered ClassC
I am in A. Leaving A...
Leaving ClassC
Leaving ClassB
Leaving ClassD
C3 applies the divide and conquer approach to calculate linearization in the following way: let A be a
class that inherits from the base classes B1, B2, … Bn. The linearization of A is the sum of A plus the
merge of the linearizations of the parents and the list of the parents:
head(XYZ) = X
tail(XYZ) = YZ
head(X) = X
tails(X) = None
In [7]: class A:
def process(self):
print('A process()')
class B:
def process(self):
print('B process()')
class C(A, B):
pass
# def process(self):
# print('C process()')
class D(C,B):
pass
obj = D()
obj.process()
print(D.mro())
A process()
[<class '__main__.D'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main_
_.B'>, <class 'object'>]
In [7]: class A:
def process(self):
print('A process()')
class B(A):
pass
class C(A):
def process(self):
print('C process()')
class D(B,C):
pass
obj = D()
obj.process()
print(D.mro())
#rocess()
C process()
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main_
_.A'>, <class 'object'>]
In [11]: class A:
def process(self):
print('A process()')
class B(A):
def process(self):
print('B process()')
class C(A, B):
pass
obj = C()
#print(C.mro())
obj.process()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Input In [11], in <cell line: 11>()
7 def process(self):
8 print('B process()')
---> 11 class C(A, B):
12 pass
15 obj = C()
In [1]: class A:
pass
class B:
pass
class C:
pass
class D(A,B):
pass
class E(B,C):
pass
class F(D,E):
pass
print(F.mro())
In [2]: l = []
l[0]=l[0]+5
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Input In [2], in <cell line: 2>()
1 l = []
----> 2 l[0]=l[0]+5
class CEmployee(Employee):
def __init__(self,n,s,t):
Employee.__init__(self,n,s)
self.b = 0.05*s
e1 = Employee('x',100)
e2 = Employee('y',200)
c1 = CEmployee('x',50,'1/1/2023')
print(e1.getBonus())
print(c1.b)
10.0
2.5
In [10]: x = input()
print(len(x)/2)
print(x[:len(x)/2])
abcd
2.0
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Input In [10], in <cell line: 3>()
1 x = input()
2 print(len(x)/2)
----> 3 print(x[:len(x)/2])
f([(1,2,3), (6,3,4),(9,3,4)])
[6, 3, 4]
In [4]: x=int(input())
y=tuple(x)
print(y)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Input In [4], in <cell line: 2>()
1 x=int(input())
----> 2 y=tuple(x)
3 print(y)
In [7]: l1=[3,5,2,1,2,1,5]
s1={1,2,5,3}
l2=[]
for i in range((l1.len())):
for j in range(len(s1)):
if(s1[j] == l1[i]):
l2.append(l1[i])
del s1[j]
print(l2)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Input In [7], in <cell line: 4>()
2 s1={1,2,5,3}
3 l2=[]
----> 4 for i in range((l1.len())):
5 for j in range(len(s1)):
6 if(s1[j] == l1[i]):
In [ ]: