Module 4-Classes, Objects &inheretace-Updated
Module 4-Classes, Objects &inheretace-Updated
Class
Belongs To
Belongs To
Uses
Uses
Uses
Uses
Belongs To
Belongs To
Data
Object
Object
>>blank = Point()
>>blank.x = 3.0
>>blank.y = 4.0
>>def print_point(p):
print('(%g, %g)' % (p.x, p.y))
>>print_point(blank)
>> def distace(p):
print(math.sqrt(p.x**2 + p.y**2))
>> distace(blank) 18 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Rectangles
Imagine you are designing a class to represent
rectangles.
What attributes would you use to specify the location
and size of a rectangle? You can ignore angle; to keep
things simple, assume that the rectangle is either
vertical or horizontal.
There are at least two possibilities:
You could specify one corner of the rectangle (or the center),
the width, and the height.
You could specify two opposing corners.
box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()
box.corner.x = 0.0
box.corner.y = 0.0
20 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Instances as return values
Functions can return instances. For example, find_center
takes a Rectangle as an argument and returns a Point that
contains the coordinates of the center of the Rectangle:
def find_center(rect):
p = Point()
p.x = rect.corner.x + rect.width/2
p.y = rect.corner.y + rect.height/2
return p
Here is an example that passes box as an argument and
assigns the resulting Point to center:
>>> center = find_center(box)
>>> print_point(center)
(50, 100)
21 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Rectangles
>>>class Rectangle:
"""Represents a rectangle.
attributes: width, height, corner.""“
>>>box = Rectangle()
>box.width = 100.0
>box.height = 200.0
>>box.corner = Point()
>>box.corner.x = 0.0
>>box.corner.y = 0.0
>>def find_center(rect):
p = Point()
p.x = rect.corner.x + rect.width/2
p.y = rect.corner.y + rect.height/2
return p
>> center = find_center(box)
>>> print_point(center)
22 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Objects are mutable
You can change the state of an object by making an
assignment to one of its attributes.
For example, to change the size of a rectangle without
changing its position, you can modify the values of
width and height:
box.width = box.width + 50
box.height = box.height + 100
This operation is called a shallow copy because it copies the object and any references it
contains, but not the embedded point.
27 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Copying
For most applications, this is not what you want. This
behavior is confusing and error-prone.
Fortunately, the copy module provides a method
named deepcopy that copies not only the object but
also the objects it refers to, and the objects they refer
to, and so on.
You will not be surprised to learn that this operation is
called a deep copy.
>>> p2=Point(3,4)
>>> p2.print_point()
(3, 4)
31 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
class Point
>>>class Point:
def __init__(self, x=0, y=0):
self.x=x
self.y=y
def print_point(self):
print('(%g, %g)' % (self.x, self.y))
def distace_from_origin(self):
return ((self.x**2) + (self.y**2))**0.5
>>> p5=Point(30,40)
>>> p5.print_point()
>>>p5.distace_from_origin()
32 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
__str__method
__str__ is a special method, like __init__, that is supposed
to return a string representation of an object.
If a class provides a method named __str__, it overrides
the default behavior of the Python built-in str function.
Printing a Point object implicitly invokes __str__ on the
object,
>>> C
>>> p=Point(3,4)
>>> print(p)
>>>p=Point(3,4)
>>>p.distace_from_origin()
5.0
>>> print(p)
(3,4)
34 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Instances as parameters
You can pass an instance as a parameter to a function
in the usual way.
For example:
def print_point(self,p):
print('(%g, %g)' % (p.x, p.y))
O/P
O/P
49 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Prototype development versus planning
In the previous slides, we demonstrated an approach to program
development that we call prototype development.
The development plan I am demonstrating is called “prototype
and patch”.
For each function, I wrote a prototype that performed the
basic calculation and then tested it, patching errors along the
way.
This approach can be effective, especially if you don’t yet have a
deep understanding of the problem. But incremental
corrections can generate code that is unnecessarily complicated
—since it deals with many special cases—and unreliable—
since it is hard to know if you have found all the errors.
An alternative is planned development, in which high-level
insight into the problem can make the programming much easier.
defining characteristics:
Programs include class and method definitions.
Objects often represent things in the real world, and methods(operations on objects) often
For example, the Time class defined already corresponds to the way people record
the time of day, and the functions we defined correspond to the kinds of things
Similarly, the Point and Rectangle classes defined already correspond to the
types.
Methods are semantically the same as functions, but there are two
syntactic differences:
Methods are defined inside a class definition in order to make the relationship
function.
class Time:
def print_time(self):
print("%d: %d: %d” % (self.hour,self.minute,self.second))
start = Time()
start.hour = 6
start.minute = 45
start.second = 30
start.print_time()
58 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
print_time & increament-methods inside the class time definition
class Time:
def print_time(self):
print("%d: %d: %d" % (self.hour,self.minute,self.second))
def increment(self, seconds):
self.second += seconds
if self.second >= 60:
self.second -= 60
self.minute += 1
if self.minute >= 60:
self.minute -= 60
self.hour += 1
start = Time()
start.hour = 9
start.minute = 45
start.second = 30
start.print_time()
start.increment(30)
start.print_time()
59 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
print_point, increament-methods inside the class Point definition &
find_center –method inside the class Rectangle definition
import math
class Rectangle:
class Point: def find_center(self):
def print_point(self): p = Point()
print('(%g, %g)' % (self.x, self.y)) p.x = self.corner.x + self.width/2
def distance(self): p.y = self.corner.y + self.height/2
return p
print(math.sqrt(self.x**2 + self.y**2))
box = Rectangle()
p2=Point() box.width = 100.0
p2.x=3 box.height = 200.0
box.corner = Point()
p2.y=4
box.corner.x = 0.0
p2.print_point() box.corner.y = 0.0
p2.distance() c=box.find_center()
c.print_point()
p2=Point()
p2.print_point()
p3=Point(3,4)
p3.print_point()
p3.distance() 62 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Using __init__ method to time class
class Time:
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
def print_time(self):
print("%d: %d: %d" % (self.hour,self.minute,self.second))
Time1=Time()
Time1.print_time()
Time2=Time(3,45,45)
Time2.print_time()
Time3=Time(9, 45,30)
Time3.print_time()
63 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Using __init__ method to time class
class Time:
def __init__(self, hour=0, minute=0, Time1=Time()
second=0): Time1.print_time()
self.hour = hour Time1.increment(30)
self.minute = minute Time1.print_time()
self.second = second
def print_time(self): Time2=Time(3,45,45)
print("%d: %d: %d" % Time2.print_time()
(self.hour,self.minute,self.second)) Time2.increment(30)
def increment(self, seconds): Time2.print_time()
self.second += seconds
Time3=Time(9, 45,30)
if self.second >= 60:
Time3.print_time()
self.second -= 60
Time3.increment(30)
self.minute += 1 Time3.print_time()
if self.minute >= 60:
self.minute -= 60
self.hour += 1
64 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
__str__method in a class
class Time:
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
__str__ is a special method, like __init__, that is supposed to return a string representation of an object.
Note: print function always calls python’s built-in str function while printing
If a class provides a method named __str__, it overrides the default behaviour of the Python built-in str
function.
Printing a Time object implicitly invokes __str__ on the object, so defining __str__ also changes the behavior
of print
65 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Using __str__method in a Time class
class Time:
def __init__(self, hour=0, minute=0, Time1=Time()
second=0): print(Time1)
self.hour = hour Time1.increment(30)
print(Time1)
self.minute = minute
self.second = second Time2=Time(3,45,45)
def __str__(self): print(Time2)
Time2.increment(30)
return '%.2d:%.2d:%.2d' % (self.hour,
print(Time2)
self.minute, self.second)
Time3=Time(9, 45,30)
def increment(self, seconds): print(Time2)
self.second += seconds Time3.increment(30)
print(Time3)
if self.second >= 60:
self.second -= 60
self.minute += 1
if self.minute >= 60:
self.minute -= 60
self.hour += 1
an argument:
print(time.is_after(other))
67 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
is_after method
class Time:
time = Time(11, 45,50)
def __init__(self, hour=0, minute=0, second=0): print(time)
self.hour = hour other1=Time(9, 45,50)
self.minute = minute print(other1)
self.second = second print(time.is_after(other1))
When you apply the + operator to Time objects, Python invokes __add__. When you print the result, Python
invokes __str__. So there is a lot happening behind the scenes!
Changing the behavior of an operator so that it works with programmer-defined types is called operator
overloading.
For every operator in Python there is a corresponding special method, like __add__.
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
def time_to_int(self):
minutes = self.hour * 60 + self.minute
seconds = minutes * 60 + self.second
return seconds
def int_to_time(self,seconds):
time = Time()
minutes, time.second = divmod(seconds, 60)
time.hour, time.minute = divmod(minutes, 60)
return time
71 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Continued--Operator overloading
def __add__(self, other):
seconds = self.time_to_int() + other.time_to_int()
return self.int_to_time(seconds)
start = Time(9,15,10)
duration = Time(9, 15,10)
print(start+duration)
print(start-duration)
72 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Operator overloading
O/P
74 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Type-based dispatch
• In the previous section we added two Time objects, but you also might want to add an integer
to a Time object.
The following is a version of __add__ that checks the type of other and invokes either add_time
or increment:
The built-in function isinstance takes a value and a class object, and returns True if the value is an
instance of the class.
If other is a Time object, __add__ invokes add_time. Otherwise it assumes that the parameter is a number
and invokes increment. This operation is called a type-based dispatch because it dispatches the
computation to different methods based on the type of the arguments.
def int_to_time(self,seconds):
time = Time()
minutes, time.second = divmod(seconds, 60)
time.hour, time.minute = divmod(minutes, 60)
return time 76 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Type-based dispatch
Unfortunately, this implementation of addition is not commutative. If the
integer is the first operand,
>>> print(1337 + start)
you get TypeError: unsupported operand type(s) for +: 'int' and 'instance'
The problem is, instead of asking the Time object to add an integer, Python
is asking an integer to add a Time object, and it doesn’t know how.
But there is a clever solution for this problem: the special method __radd__,
which stands for “right-side add”.
This method is invoked when a Time object appears on the right side of
the + operator. Here’s the definition:
def time_to_int(self):
minutes = self.hour * 60 + self.minute
seconds = minutes * 60 + self.second
return seconds
def histogram(s):
d = dict()
for c in s:
if c not in d:
d[c] = 1
else:
d[c] = d[c]+1
return d
This function works for strigns, and also works for lists, tuples, and even dictionaries,
as long as the elements of s are hashable, so they can be used as keys in d.
Functions that work with several types are called polymorphic.
Polymorphism can facilitate code reuse.
s1=“engineering"
x=histogram(s1)
print(x)
O/P
82 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Interface and implementation
One of the goals of object-oriented design is to make software more maintainable, which means
that you can keep the program working when other parts of the system change, and modify the
program to meet new requirements.
A design principle that helps achieve that goal is to keep interfaces separate from
implementations.
For objects, that means that the methods a class provides should not depend on how the
attributes are represented.
For example, in this chapter we developed a class that represents a time of day. Methods provided
by this class include time_to_int, is_after, and add_time.
We could implement those methods in several ways. The details of the implementation depend
on how we represent time.
In this chapter, the attributes of a Time object are hour, minute, and second. As an alternative,
we could replace these attributes with a single integer representing the number of seconds since
midnight. This implementation would make some methods, like is_after, easier to write, but it
makes other methods harder.
After you deploy a new class, you might discover a better implementation. If other parts of the
program are using your class, it might be time-consuming and error-prone to change the interface.
But if you designed the interface carefully, you can change the implementation without
changing the interface, which means that other parts of the program don’t have to change.
class Card:
def __init__(self, suit=0, rank=0):
self.suit = suit
self.rank = rank
To create a Card, you call Card with the suit and rank of the card you want.
Variables like suit_names and rank_names, which are defined inside a class but
outside of any method, are called class attributes because they are associated with
the class object Card.
This term distinguishes them from variables like suit and rank, which are called
instance attributes because they are associated with a particular instance.
88 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Class attributes
The above figure indicates a diagram of the Card class object and one Card instance.
• Card is a class object; its type is type. card1 is an instance of Card, so its type is Card.
• Every card has its own suit and rank, but there is only one copy of suit_names and
rank_names.
• Putting it all together, the expression Card.rank_names[self.rank] means “use the attribute
rank from the object self as an index into the list rank_names from the class Card, and
select the appropriate string.”
• The first element of rank_names is None because there is no card with rank zero. By
including
None as a place-keeper, we get a mapping with the nice property that the index 2 maps to the
string '2', and so on. With the methods we have so far, we can create and print cards:
>>> card1 = Card(2, 11) 89 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
>>> print(card1)
Class attributes
class Card:
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank
O/P
Jack of Hearts
def __str__(self):
return '%s of %s' % (Card.rank_names[self.rank],Card.suit_names[self.suit])
def __str__(self):
return '%s of %s' % (Card.rank_names[self.rank],Card.suit_names[self.suit])
print(card1)
print(card2)
print(card3)
print(card1<card2)
print(card1<card3) 96 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Decks
Now that we have Cards, the next step is to define Decks. Since a deck is made up of
cards, it is natural for each Deck to contain a list of cards as an attribute.
The following is a class definition for Deck. The init method creates the attribute cards and
generates the standard set of fifty-two cards:
class Deck:
def __init__(self):
self.cards = []
for suit in range(4):
for rank in range(1, 14):
card = Card(suit, rank)
self.cards.append(card)
• The easiest way to populate the deck is with a nested loop. The outer loop enumerates the
suits from 0 to 3. The inner loop enumerates the ranks from 1 to 13. Each iteration creates
a new Card with the current suit and rank, and appends it to self.cards.
deck = Deck()
print(deck)
To deal cards, we would like a method that removes a card from the deck and returns it.
The list method pop provides a convenient way to do that:
def pop_card(self):
return self.cards.pop()
Since pop removes the last card in the list, we are dealing from the bottom of the deck.
As another example, we can write a Deck method named shuffle using the function shuffle
from the random module:
def shuffle(self):
random.shuffle(self.cards)
class Deck:
def __init__(self):
self.cards = []
for suit in range(4):
for rank in range(1, 14):
card = Card(suit, rank)
self.cards.append(card)
def shuffle(self):
random.shuffle(self.cards)
103 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Inheritance
Inheritance is the ability to define a new class that is a modified version of an existing class
As an example, let’s say we want a class to represent a “hand”, that is, the cards held by one player.
A hand is similar to a deck: both are made up of a collection of cards, and both require operations like
adding and removing cards.
A hand is also different from a deck; there are operations we want for hands that don’t make sense for a
deck. For example, in poker we might compare two hands to see which one wins. In bridge, we might
compute a score for a hand in order to make a bid.
To define a new class that inherits from an existing class, you put the name of the existing
class in parentheses:
class Hand(Deck):
"""Represents a hand of playing cards.""“
This definition indicates that Hand inherits from Deck; that means we can use methods like
The other methods are inherited from Deck, so we can use pop_card and add_card to
deal
a card:
>>> deck = Deck()
>>> card = deck.pop_card()
>>> hand.add_card(card)
>>> print(hand)
King of Spades 105 © Dr.S.Gowrishankar, Dept. of CSE, Dr.AIT
Inheritance
import random
class Card:
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank
suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7','8', '9', '10', 'Jack', 'Queen', 'King']
def __str__(self):
return '%s of %s' % (Card.rank_names[self.rank],Card.suit_names[self.suit])
class Deck:
def __init__(self):
self.cards = []
for suit in range(4):
for rank in range(1, 14):
card = Card(suit, rank)
self.cards.append(card)
• The arrow with a hollow triangle head represents an IS-A relationship; in this case it
indicates that Hand inherits from Deck.
• The standard arrow head represents a HAS-A relationship; in this case a Deck has
references to Card objects.
• The star (*) near the arrow head is a multiplicity; it indicates how many Cards a
Deck has. A multiplicity can be a simple number, like 52, a range, like
• There are no dependencies in this diagram. They would normally be shown with a
dashed arrow. Or if there are a lot of dependencies, they are sometimes omitted.