Demonstrating Single Inheritance. Student is the baseclass and PGStudent is the subclass.
PGStudent
is a Student. The state and behaviour of Student is inherited by PGStudent. PGStudent has additional
state and additional methods.
In [1]: #%% Single Inheritance Example: Base class Student
class Student:
def __init__(self,idNo,name,fee):
self.id=idNo
self.name=name
self.fee=fee
self.phone = ""
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
ID:001 Name:Rahul Fee:50000
Name: Rohan
Gate Score: 99
ID:002 Name:Rohan Fee:40000 Gate Score:99
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 __init__(self, sides):
self.n = len(sides)
self.sideLengths =sides
def noofsides(self):
return self.n
def perimeter(self):
sum=0
for i in range(self.n):
sum += self.sideLengths[i]
return sum
#to define an abstract method to calculate area.
@abstractmethod
def area(self):
pass
class Triangle(Polygon):
# define with 3 sides
def __init__(self, s1,s2,s3):
Polygon.__init__(self, [s1,s2,s3])
# overriding abstract method
def area(self):
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])
# overriding abstract method
def area(self):
return self.sideLengths[0]*self.sideLengths[1]
class Square(Polygon):
# define with 1 side length
def __init__(self, s):
Polygon.__init__(self, [s,s,s,s])
# overriding abstract method
def area(self):
return self.sideLengths[0]*self.sideLengths[0]
# 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)
Sides: 3 Perimeter: 14 Area: 7.483314773547883
Sides: 4 Perimeter: 16 Area: 15
Sides: 4 Perimeter: 20 Area: 25
---------------------------------------------------------------------------
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.
In [1]: from abc import ABC, abstractmethod
import math
class P(ABC):
def __init__(self, sides):
self.n = len(sides)
self.sideLengths =sides
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):
def __init__(self, sides):
self.n = len(sides)
self.sideLengths =sides
@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()
TypeError: Can't instantiate abstract class P with abstract method noofsides
In [4]: from abc import ABC, abstractmethod
import math
class P1(ABC):
def __init__(self, sides):
self.n = len(sides)
self.sideLengths =sides
@abstractmethod
def noofsides(self):
return self.n
class P2(P1):
def __init__(self, sides):
P1.__init__(self,sides)
def noofsides(self):
return super().noofsides()
p2 = P2([1,2,3])
p2.noofsides()
Out[4]: 3
Multiple Inheritance Example:base classes Robot, Dog. subclass:RoboDog
In [2]: #%% Multiple Inheritance:
class Robot:
def __init__(self, name, model_no, creator):
self.name = name
self.model_no = model_no
self.creator = creator
def walk(self):
return 'I am walking using my wheels'
def charge(self):
return 'I am charging... \nCharging Completed.'
class Dog:
def __init__(self, name, height, weight, species=None):
self.name = name
self.species = species
self.height = height
self.weight = weight
def bark(self):
return "I'm barking"
def walk(self):
return "I'm walking with my legs"
class RoboDog(Robot, Dog):
def __init__(self, name, model_no, creator, height, weight):
Robot.__init__(self, name, model_no, creator)
Dog.__init__(self, name, height, weight)
def walk(self):
# we can use behaviour from any one of the base cases.
res= Robot.walk(self) +"..."+ Dog.walk(self)
return res
Pika = RoboDog('Pika', 'rd-t1', 'Robo-Labs', '2', '5')
print(Pika.bark())
print(Pika.charge())
print(Pika.walk())
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.
In [2]: class A1:
def process(self):
print('A1 process()')
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.
Option 1:Programmer explicitly specifies which parent class to be used.
Result: Method resolved based on the parent class specified. Experiment by changing the parent class
name.
In [24]: 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):
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...
Method resolution gets complicated when multiple levels of inheritance is used.
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'>]
Case:2 described above.
In [4]: #%% Diamond problem- class diamond structure
class A:
def go(self):
return 'I am super class-A'
class B(A):
pass
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
Call to base class methods using the super().
In [4]: class A:
def f(self):
print('I am in A. Leaving A...')
class B(A):
def f(self):
print('Entered ClassB')
super().f()
print('Leaving ClassB')
class C(A):
def f(self):
print('Entered ClassC')
super().f()
print('Leaving ClassC')
class D(B, C):
def f(self):
print('Entered ClassD')
super().f() # case 1
# B.f(self) # case 2
# C.f(self) # case 3
print('Leaving ClassD')
print("The Method Resolution Order:",D.mro())
obj_d = D()
obj_d.f()
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:
L[A(B1 … Bn)] = A + merge(L[B1],L[B2], … L[Bn], [B1 … Bn])
head(XYZ) = X
tail(XYZ) = YZ
head(X) = X
tails(X) = None
To perform the merge:
Look at the head of the first list: L[B1][0] If this head is a “good head”, means that it does not appear in
the tail of any other list - add it to the linearization of A and remove it from all the lists in the merge.
Otherwise, look at the next list’s head and if it is a “good head”, add it to the linearization Repeat until all
the classes are removed or there are no good heads left. In the latter case, the construction fails.
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()
TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, B
To fix, you may need to change the inheritance order.
In [9]: class A:
def process(self):
print('A process()')
class B(A):
def process(self):
print('B process()')
class C(B,A):
pass
obj = C()
print(C.mro())
obj.process()
[<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'objec
t'>]
B process()
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())
[<class '__main__.F'>, <class '__main__.D'>, <class '__main__.A'>, <class '__main_
_.E'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]
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
IndexError: list index out of range
In [3]: d = dict()
d['01']=['x',22]
print(d)
{'01': ['x', 22]}
In [14]: class Employee:
def __init__(self,n,s):
self.n = n
self.s = s
self.b = 0.1*s
def getBonus(self):
return self.b
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])
TypeError: slice indices must be integers or None or have an __index__ method
In [13]: def f(l):
t1 = list(l[1][:])
print(t1)
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)
TypeError: 'int' object is not iterable
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]):
AttributeError: 'list' object has no attribute 'len'
In [ ]: