0% found this document useful (0 votes)
7 views

Inheritance Notes

Uploaded by

pardhapradeep824
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
7 views

Inheritance Notes

Uploaded by

pardhapradeep824
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 20

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 [ ]: ​

You might also like