Module IV Python
Module IV Python
MODULE IV
MODULE IV
Programmer-defined Types
We have used many of Python’s built-in types; now we are going to define a new type.
Lets create our own user-defined type: Point.
Consider the concept of a mathematical point. In two dimensions, a point is two numbers (coordinates)
that are treated collectively as a single object/entity.
In mathematical notation, points are often written in parentheses with a comma separating the
coordinates. For example, (0, 0) represents the origin, and (x, y) represents the point x units to the right
and y units up from the origin.
There are several ways we might represent points in Python:
o We could store the coordinates separately in two variables, x and y.
o We could store the coordinates as elements in a list or tuple.
o We could create a new type to represent points as objects.
A programmer-defined new type is also called a class
A class in Python can be created using a keyword class.
The syntax rules for a class definition is:
o There is a header which begins with the keyword, class, followed by the name of the class,
and ending with a colon.
print(Point)
The output would be –
<class ' main .Point'>
The term main indicates that the class Point is in the main scope of the current module.
In other words, this class is at the top level while executing the program.
Now, a user-defined data type Point got created, and this can be used to create any number of
objects of this class.
Observe the following statements:
p=Point()
Now, a reference (for easy understanding, treat reference as a pointer) to Point object is created
and is returned. This returned reference is assigned to the object p.
The process of creating a new object is called as instantiation and the object is instance of a
class.
Classes, like functions, are callable, and we instantiate a Point object by calling the Point class
When we print an object, Python tells which class it belongs to and where it is stored in the
memory.
print(p)
The output would be –
< main .Point object at 0x003C1BF0>
The output displays the address (in hexadecimal format) of the object in the memory.
It is now clear that, the object occupies the physical space, whereas the class does not.
Attributes
User-defined classes in Python have two types of attributes viz. class attributes and instance
attributes.
Class attributes are defined inside the class (usually, immediately after class header). They are
common to all the objects of that class. That is, they are shared by all the objects created from that
class.
But, instance attributes defined for individual objects. They are available only for that instance (or
object).
Attributes of one instance are not available for another instance of the same class.
An object can contain named elements known as instance attributes.
One can assign values to these attributes using dot operator.
For example, keeping coordinate points in mind, we can assign two attributes x and y for the
object of a class Point as below
p.x =10.0
p.y =20.0
A state diagram that shows an object and its attributes is called as object diagram.
For the object p, the object diagram is shown in Figure below.
Point
x 10.0
y 20.0
p
The diagram indicates that a variable (i.e. object) p refers to a Point object, which contains two
attributes.
Each attributes refers to a floating point number.
One can access attributes of an object as shown –
>>> print(p.x)
10.0
>>> print(p.y)
20.0
Here, p.x means “Go to the object p refers to and get the value of x”.
Attributes of an object can be assigned to other variables
>>> x= p.x
>>> print(x)
10.0
Here, the variable x is nothing to do with attribute x.
There will not be any name conflict between normal program variable and attributes of an object.
A complete program:
Write a class Point representing a point on coordinate system. Implement following functions –
A function read_point() to receive x and y attributes of a Point object as user input.
A function distance() which takes two objects of Point class as arguments and computes the
Euclidean distance between them.
A function print_point() to display one point in the form of ordered-pair.
Program:
import math
class Point:
""" This is a class Point representing a coordinate point"""
#function which takes two objects of Point class as arguments and computes the Euclidean
distance between them
def distance(p1,p2):
d=[Link]((p1.x-p2.x)**2+(p1.y-p2.y)**2)
return d
print("Distance is: %g" % dist) #print the Euclidean distance between p1 and p2
x1 x2
2
y1 y2
2
In this program, we have Point objects as (p1.x, p1.y) and (p2.x, p2.y). Apply the formula on
these points by passing objects p1 and p2 as parameters to the function distance(). And then
return the result.
Thus, the above program gives an idea of defining a class, instantiating objects, creating attributes,
defining functions that takes objects as arguments and finally, calling (or invoking) such functions
whenever and wherever necessary.
NOTE: User-defined classes in Python have two types of attributes viz. class attributes and instance
attributes. Class attributes are defined inside the class (usually, immediately after class header). They
are common to all the objects of that class. That is, they are shared by all the objects created from that
class. But, instance attributes defined for individual objects. They are available only for that instance (or
object). Attributes of one instance are not available for another instance of the same class.
For example, consider the class Point as discussed earlier –
class Point:
pass
This clearly indicates that the attributes x and y created are available only for the object p1, but
not for p2. Thus, x and y are instance attributes but not class attributes.
We will discuss class attributes late in-detail. But, for the understanding purpose, observe the
following example –
class Point:
x=2
y=3
Here, the attributes x and y are defined inside the definition of the class Point itself. Hence, they
are available to all the objects of that class.
Rectangles
It is possible to make an object of one class as an attribute to other class.
To illustrate this, consider an example of creating a class called as Rectangle.
A rectangle can be created using any of the following data –
By knowing width and height of a rectangle and one corner point (ideally, a bottom- left
corner) in a coordinate system
By knowing two opposite corner points
Let us consider the first technique and implement the task:
Program : Write a class Rectangle containing numeric attributes width and height. This class
should contain another attribute corner which is an instance of another class Point. Implement
following functions –
o A function to print corner point as an ordered-pair
o A function find_center() to compute center point of the rectangle
o A function resize() to modify the size of
rectangle
The program is as given below –
class Point:
""" This is a class Point representing coordinate point"""
class Rectangle:
""" This is a class Rectangle. Attributes: width, height and Corner Point """
def find_center(rect):
"""Returns a Point at the center of a Rectangle.
def print_point(p):
print("(%g,%g)"%(p.x, p.y))
center=find_center(box)
print("The center of rectangle is:")
print_point(center)
resize(box,50,70)
print("Rectangle after resize:")
print("width=%g, height=%g"%([Link], [Link]))
center=find_center(box)
print("The center of resized rectangle is:")
print_point(center)
In this program, we are treating the corner point as the origin in coordinate system and hence
the following assignments –
[Link].x=0 [Link].y=0
(Note that, instead of origin, any other location in the coordinate system can be given as
corner point.) Based on all above statements, an object diagram can be drawn as –
Rectangle
width 100 Point
height 200
box x 0
corner y 0
The expression [Link].x means, “Go to the object box refers to and select the attribute
named corner; then go to that object and select the attribute named x.”
The function find_center() takes an object rect as an argument. So, when a call is made using
the statement –
center=find_center(box)
the object rect acts as an alias for the argument box.
A local object center of type Point has been created inside this function. The attributes of center
are x and y, which takes the values as the coordinates of center point of rectangle. Center of a
rectangle can be computed with the help of following diagram.
The function find_center() returns the computed center point. Note that, the return value of a
function here is an instance of some class. That is, one can have an instance as return values
from a function.
The function resize() takes three arguments: rect – an instance of Rectangle class and two
numeric variables w and h. The values w and h are added to existing attributes width and
height. This clearly shows that objects are mutable. State of an object can be changed by
modifying any of its attributes. When this function is called with a statement –
resize(box,50,70)
the rect acts as an alias for box. Hence, width and height modified within the function will
reflect the original object box.
Thus, the above program illustrates the concepts: Object of one class is made as attribute for object of
another class, returning objects from functions and objects are mutable.
Copying
An object will be aliased whenever there an object is assigned to another object of same class.
This may happen in following situations –
Direct object assignment (like p2=p1)
When an object is passed as an argument to a function
When an object is returned from a function
The last two cases have been understood from the two programs in previous sections.
Let us understand the concept of aliasing more in detail using the following program
>>> class Point:
pass
>>> p1=Point()
>>> p1.x=10
>>> p1.y=20
>>> p2=p1
>>> print(p1)
< main .Point object at 0x01581BF0>
>>> print(p2)
< main .Point object at 0x01581BF0>
Observe that both p1 and p2 objects have same physical memory. It is clear now that the object p2
is an alias for p1.
So, we can draw the object diagram as below –
p1 x 10 p2
y 20
Hence, if we check for equality and identity of these two objects, we will get following result.
>>> p1 is p2
True
>>> p1==p2
True
But, the aliasing is not good always. For example, we may need to create a new object using an
existing object such that – the new object should have a different physical memory, but it must have
same attribute (and their values) as that of existing object. Diagrammatically, we need something as
below –
x 10 p2
p1 x 10 y 20
y 20
>>> p1=Point()
>>> p1.x=10
>>> p1.y=20
Observe that the physical address of the objects p1 and p3 are now different.
But, values of attributes xand y are same. Now, use the following statements –
>>> p1 is p3
False
>>> p1 == p3
False
Here, the is operator gives the result as False for the obvious reason of p1 and p3 are being two
different entities on the memory.
And, Python cannot understand the meaning of equality on the new data type. The default behavior
of equality (==) is identity (is operator) itself. Hence, Python applies this default behavior on p1 ==
p3and results in False.
NOTE: If we need to define the meaning of equality (==) operator explicitly on user-defined data types
(i.e. on class objects), then we need to override the method eq () inside the class. This will be
discussed later in detail.
The copy() method of copy module duplicates the object.
The content (i.e. attributes) of one object is copied into another object as we have discussed till
now.
But, when an object itself is an attribute inside another object, the duplication will result in a
strange manner.
To understand this concept, try to copy Rectangle object (created in previous section) as given
below
class Rectangle:
""" This is a class [Link]: width, height and Corner Point """
box1=Rectangle()
[Link]=Point()
[Link]=100
[Link]=200
[Link].x=0
[Link].y=0
box2=[Link](box1)
print(box1 is box2) #prints False
print([Link] is [Link]) #prints True
Now, the question is – why [Link] and [Link] are same objects, when box1 and box2
are different? Whenever the statement is executed,
box2=[Link](box1)
The contents of all the attributes of box1 object are copied into the respective attributes of box2
object.
That is, [Link] is copied into [Link], [Link] is copied into [Link].
Similarly, [Link] is copied into [Link].
Now, recollect the fact that corner is not exactly the object itself, but it is a reference to the object
of type Point (Read the discussion done for Figure at the beginning of this Chapter).
Hence, the value of reference (that is, the physical address) stored in [Link] is copied into
[Link].
Thus, the physical object to which [Link] and [Link] are pointing is only one.
This type of copying the objects is known as shallow copy.
To understand this behavior, observe the following object diagram :
Now, the attributes width and height for two objects box1 and box2 are independent.
Whereas, the attribute corner is shared by both the objects.
Thus, any modification done to [Link] will reflect [Link] as well.
Obviously, we don’t want this to happen, whenever we create duplicate objects. That is, we want
two independent physical objects.
Python provides a method deepcopy() for doing this task.
This method copies not only the object but also the objects it refers to, and the objects they refer
to, and so on.
box3=[Link](box1)
print(box1 is box3) #prints False
print([Link] is [Link]) #prints False
Thus, the objects box1 and box3 are now completely independent.
Debugging
While dealing with classes and objects, we may encounter different types of errors.
For example, if we try to access an attribute which is not there for the object, we will get
AttributeError. For example –
>>> p= Point()
>>> p.x = 10
>>> p.y = 20
>>> print(p.z)
AttributeError: 'Point' object has no attribute 'z'
To avoid such error, it is better to enclose such codes within try/except as given below –
try:
z = p.x
except AttributeError:
z=0
When we are not sure, which type of object it is, then we can use type() as –
>>> type(box1)
<class ' main .Rectangle'>
Another method isinstance() helps to check whether an object is an instance of a particular class
>>> isinstance(box1,Rectangle)
True
When we are not sure whether an object has a particular attribute or not, use a function hasattr() –
>>> hasattr(box1, 'width')
True
Observe the string notation for second argument of the function hasattr(). Though the attribute
width is basically numeric, while giving it as an argument to function hasattr(), it must be enclosed
within quotes.
Pure Functions
To understand the concept of pure functions, let us consider an example of creating a class called
Time.
An object of class Time contains hour, minute and second as attributes.
Write a function to print time in HH:MM:SS format and another function to add two time objects.
Note that, adding two time objects should yield proper result and hence we need to check whether
number of seconds exceeds 60, minutes exceeds 60 etc, and take appropriate action.
class Time:
"""Represents the time of a day Attributes: hour, minute, second """
def printTime(t):
print("%.2d:%.2d:%.2d"%([Link],[Link],[Link]))
def add_time(t1,t2):
sum=Time()
[Link] = [Link] + [Link]
[Link] = [Link] + [Link]
t1=Time()
[Link]=10
[Link]=34
[Link]=25
print("Time1 is:")
printTime(t1)
t2=Time()
[Link]=2
[Link]=12
[Link]=41
print("Time2 is :")
printTime(t2)
t3=add_time(t1,t2)
print("After adding two time objects:")
printTime(t3)
Here, the function add_time() takes two arguments of type Time, and returns a Time object, whereas,
it is not modifying contents of its arguments t1 and t2.
Such functions are called as pure functions.
Modifiers
Sometimes, it is necessary to modify the underlying argument so as to reflect the caller.
That is, arguments have to be modified inside a function and these modifications should be
available to the caller.
The functions that perform such modifications are known as modifier function.
Assume that, we need to add few seconds to a time object, and get a new time. Then, we can
write a function as below
def valid_time(time):
if [Link] < 0 or [Link] < 0 or [Link] < 0:
return False
return True
Now, at the beginning of add_time() function, we can put a condition as –
def add_time(t1, t2):
if not valid_time(t1) or not valid_time(t2):
raise ValueError('invalid Time object in add_time')
seconds = time_to_int(t1) + time_to_int(t2)
return int_to_time(seconds)
Python provides another debugging statement assert.
When this keyword is used, Python evaluates the statement following it. If the statement is True,
further statements will be evaluated sequentially. But, if the statement is False, then
AssertionError exception is raised.
The usage of assert is shown here –
def add_time(t1, t2):
assert valid_time(t1) and valid_time(t2)
seconds = time_to_int(t1) + time_to_int(t2)
return int_to_time(seconds)
The assert statement clearly distinguishes the normal conditional statements as a part of the logic
of the program and the code that checks for errors.
Object-Oriented Features
Python as an object oriented programming language, it must possess the following characteristics:
Programs include class and method definitions.
Most of the computation is expressed in terms of operations on objects.
Objects often represent things in the real world, and methods often correspond to the ways
objects in the real world interact.
To establish relationship between the object of the class and a function, we must define a function
as a member of the class.
For example, the Time class defined earlier corresponds to the way people record the time of day,
and the functions we defined correspond to the kinds of things people do with times
A function which is associated with a particular class is known as a method.
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
between the class and the method explicit.
The syntax for invoking a method is different from the syntax for calling a function.
Printing objects
In the last program, we defined a class named Time and wrote a function named print_time,
which should have looked something like this:
To make print_time a method, all we have to do is move the function definition inside the class
definition. Notice the change in indentation.
class Time:
def print_time(time):
#body
Now we can invoke print_time using dot notation in two ways.
Time.print_time(start) #start is Time object
start.print_time()
In this use of dot notation, print_time is the name of the method (again), and start is the object the
Another example
Here’s a version of increment rewritten as a method:
# inside class Time:
def increment(self, seconds):
seconds += self.time_to_int()
return int_to_time(seconds)
This version assumes that time_to_int is written as a method. Also, note that it is a pure function,
not a modifier.
Here’s how we would invoke increment:
>>> start.print_time()
09:45:00
>>> end = [Link](1337)
>>> end.print_time()
10:07:17
The subject, start, gets assigned to the first parameter, self. The argument, 1337, gets assigned to
the second parameter, seconds.
This mechanism can be confusing, especially if we make an error. For example, if we invoke
increment with two arguments, we get:
>>> end = [Link](1337, 460)
TypeError: increment() takes 2 positional arguments but 3 were given
The error message is initially confusing, because there are only two arguments in parentheses. But
def dist(self,p2):
d=[Link]((self.x-p2.x)**2 + (self.y-p2.y)**2)
return d
Let us understand the working of this program and the concepts involved:
Keep in mind that every method of any class must have the first argument as self. The argument
self is a reference to the current object. That is, it is reference to the object which invoked the
method. (Those who know C++, can relate self with this pointer). The object which invokes a
method is also known as subject.
The method init () inside the class is an initialization method, which will be invoked
automatically when the object gets created. When the statement like –
p1=Point(10,20)
is used, the init () method will be called automatically. The internal meaning of the above
line is –
p1. init (10,20)
Here, p1 is the object which is invoking a method. Hence, reference to this object is created and
passed to init () as self. The values 10 and 20 are passed to formal parameters a and b of init ()
method. Now, inside init () method, we have statements
self.x=10
self.y=20
This indicates, xand yare instance attributes. The value of xfor the object p1 is 10 and, the
value of y for the object p1is 20.
When we create another object p2, it will have its own set of x and y. That is, memory
locations of instance attributes are different for every object.
d=[Link](p2)
a reference to the object p1 is passed as self to dist() method and p2 is passed explicitly as a
second argument. Now, inside the dist()method, we are calculating distance between two point
(Euclidian distance formula is used) objects. Note that, in this method, we cannot use the name
p1, instead we will use self which is a reference (alias) to p1.
the ordinary print() method will print the portion “P1 is:” and the remaining portion is taken care by
str () method.
In fact, str () method will return the string format what we have given inside it, and that string
will be printed by print() method.
Operator Overloading
Some languages make it possible to change the definition of the built- in operators when they are
applied to user-defined types.
Changing the behavior of an operator so that it works with programmer-defined types is called
operator overloading or Ability of an existing operator to work on user-defined data type (class)
is known as operator overloading.
By defining other special methods, we can specify the behavior of operators on programmer defined
types.
For example, if you define a method named add for the Time class, you can use the +
operator on Time objects.
As usual, the first parameter is the object on which the method is invoked.
The second parameter is conveniently named other to distinguish it from self.
It is a polymorphic nature of any object oriented programming.
Basic operators like +, -, * etc. can be overloaded.
To overload an operator, one needs to write a method within user-defined class.
Python provides a special set of methods which have to be used for overloading required operator.
The method should consist of the code what the programmer is willing to do with the operator.
Following table shows gives a list of operators and their respective Python methods for overloading.
class Point:
def _init_ (self,a=0,b=0):
self.x=a
self.y=b
p1=Point(10,20)
p2=Point(4,5)
print("P1 is:",p1)
print("P2 is:",p2)
p4=p1+p2 #call for add () method
print("Sum is:",p4)
In the above program, when the statement p4 = p1+p2 is used, it invokes a special method _add
() written inside the class. Because, internal meaning of this statement is–
p4 = p1. add (p2)
Here, p1 is the object invoking the method. Hence, self inside _add () is the reference (alias) of p1.
And, p2 is passed as argument explicitly.
In the definition of add (), we are creating an object p3with the statement –
p3=Point()
The object p3 is created without initialization. Whenever we need to create an object with and without
initialization in the same program, we must set arguments of init () for some default values. Hence, in the
above program arguments a and b of init () are made as default arguments with values as zero. Thus, x
and y attributes of p3will be now zero. In the add () method, we are adding respective attributes of self and
other and storing in p3.x and p3.y. Then the object p3 is returned. This returned object is received as p4
and is printed
NOTE that, in a program containing operator overloading, the overloaded operator behaves in a
normal way when basic types are given. That is, in the above program, if we use the statements
m= 3+4
print(m)
it will be usual addition and gives the result as 7. But, when user-defined types are used as operands,
then the overloaded method is invoked.
Type based dispatch
Let us consider a more complicated program involving overloading.
Consider a problem of creating a class called Time, adding two Time objects, adding a number to
Time object etc. that we had considered in previous section.
Here is a complete program with more of OOP concepts.
class Time:
def init (self, h=0,m=0,s=0):
[Link]=h
[Link]=m
[Link]=s
def time_to_int(self):
minute=[Link]*60+[Link]
seconds=minute*60+[Link]
return seconds
def _ eq (self,other):
return [Link]==[Link] and [Link]==[Link] and [Link]==[Link]
T1=Time(3,40)
T2=Time(5,45)
print("T1 is:",T1)
print("T2 is:",T2)
print("Whether T1 is same as T2?",T1==T2) #call for eq ()
T3=T1+T2 #call for add ()
print("T1+T2 is:",T3)
T6=sum([T1,T2,T3,T4])
print("Using sum([T1,T2,T3,T4]):",T6)
We need to implement right-side addition method radd (). Inside this method, we can call
overloaded method add ().
The following are some of the reverse operator overloading special methods:
Polymorphism
Type-based dispatch is useful when it is necessary, but (fortunately) it is not always necessary.
Often we can avoid it by writing functions that work correctly for arguments with different
types.
Many of the functions we wrote for strings also work for other sequence types.
For example, in Module 2 we used histogram to count the number of times each letter appears in
a word.
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 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.
But if we designed the interface carefully, we can change the implementation without changing the
interface, which means that other parts of the program don’t have to change.
Debugging
We have seen earlier that hasattr() method can be used to check whether an object has particular
attribute.
There is one more way of doing it using a method vars(). This method maps attribute names and
their values as a dictionary.
For example, for the Point class defined earlier, use the statements
>>> p = Point(3, 4)
>>> vars(p) #output is {'y': 4, 'x': 3}
For purposes of debugging, you might find it useful to keep this function handy:
def print_attributes(obj):
for attr in vars(obj):
print(attr, getattr(obj, attr))
Here, print_attributes() traverses the dictionary and prints each attribute name and its
corresponding value.
The built-in function getattr() takes an object and an attribute name (as a string) and returns the
attribute values
4.4 INHERITANCE
Inheritance is the ability to define a new class from an existing class.
To define a new class that inherits from an existing class, you put the name of the existing class in
parentheses
When a new class inherits from an existing one, the existing one is called the parent/base/super
and the new class is called the child/derived/sub.
Inheritance enables us to define a class that takes all the functionality from a parent class and
allows us to add more.
Derived class inherits features from the base class where new features can be added to it. This
Inheritance and composition are two major concepts in object oriented programming that model
the relationship between two classes.
Both of them enable code reuse, but they do it in different ways.
Inheritance models what is called an is a relationship. This means that when you have
a Derived class that inherits from a Base class, you created a relationship where Derived is
Classes that contain objects of other classes are usually referred to as composites, where classes
that are used to create more complex types are referred to as components.
For example, Horse class can be composed by another object of type Tail.
Composition allows you to express that relationship by saying a Horse has a Tail.
Horse and Dog classes can leverage the functionality of Tail through composition without