0% found this document useful (0 votes)
2 views18 pages

Python Module 5

Module 5 of the Python Programming course covers Object-Oriented Programming concepts such as mutability, equality, copying, and operator overloading. It introduces user-defined types like BankAccount and MyTime, explaining how to manage object states and implement functionalities like addition and modification. The module also discusses polymorphism, highlighting its importance in designing extensible and modular systems.

Uploaded by

shamnachammu158
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views18 pages

Python Module 5

Module 5 of the Python Programming course covers Object-Oriented Programming concepts such as mutability, equality, copying, and operator overloading. It introduces user-defined types like BankAccount and MyTime, explaining how to manage object states and implement functionalities like addition and modification. The module also discusses polymorphism, highlighting its importance in designing extensible and modular systems.

Uploaded by

shamnachammu158
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Python Programming Module 5

KVG College Of Engineering

DEPARTMENT OF ECE

NOTES
PYTHON PROGRAMMING
1BPLC105B/205B

PYTHON PROGRAMMING
Module-5

[Link]
Python Programming Module 5

Object Oriented Programming


Objects are Mutable
We can change the state of an object by making an assignment to one of its attributes. For example,

class BankAccount:
def init (self, owner, balance):

[Link] = owner

[Link] = balance # state (mutable)

def deposit(self, amount):

[Link] += amount # modifies the object’s state

account = BankAccount("Alice", 1000)

print([Link]) # 1000

[Link](500) # state changed

print([Link]) # 1500

Here, BankAccount is a mutable object because its internal state (balance) can change.

Mutable objects are useful when you want to model things that naturally change (like a bank
account, a shopping cart, or a game character).

Sameness
The meaning of the word “same” seems perfectly clear until we give it some thought, and then we
realize there is more to it than we initially expected.
For example, if we say, “Alice and Bob have the same car”, we mean that her car and his are the
same make and model, but that they are two different cars. If we say, “Alice and Bob have the
same mother”, we mean that her mother and his are the same person.

When we talk about objects, there is a similar ambiguity. For example, if two Points are the same,
does that mean they contain the same data (coordinates) or that they are actually the same object?

[Link]
Python Programming Module 5

We’ve already seen the is operator in the chapter on lists, where we talked about aliases: it allows
us to find out if two references refer to the same object:

>>> p1 = Point(3, 4)
>>> p2 = Point(3, 4)
>>> p1 is p2
False
Even though p1 and p2 contain the same coordinates, they are not the same object. If we assign p1
to p3, then the two variables are aliases of the same object:

>>> p3=p1
>>> p1 is p3
True
This type of equality is called shallow equality because it compares only the references, not the
contents of the objects. To compare the contents of the objects — deep equality — we can write
a function called same_coordinates:

def same_coordinates(p1, p2):

return (p1.x == p2.x) and (p1.y == p2.y)

Now if we create two different objects that contain the same data, we can use same_point to find
out if they represent points with the same coordinates.

>>> p1 = Point(3, 4)
>>> p2 = Point(3, 4)
>>> same_coordinates(p1, p2)
True

Of course, if the two variables refer to the same object, they have both shallow and deep equality.

Python has a powerful feature that allows a designer of a class to decide what an operation like ==
or < should mean. (We’ve just shown how we can control how our own objects are converted to
strings, so we’ve already made a start!) We’ll cover more detail later. But sometimes the
implementors will attach shallow equality semantics, and sometimes deep equality, as shown in
this little experiment:

[Link]
Python Programming Module 5

p = Point(4, 2)
s = Point(4, 2)
print("== on Points returns", p == s)
# By default, == on Point objects does a shallow equality test

a = [2,3]
b = [2,3]
print("== on lists returns", a == b)
# But by default, == does a deep equality test on lists

This outputs: == on Points returns False

== on lists returns True


So we conclude that even though the two lists (or tuples, etc.) are distinct objects with different
memory addresses, for lists the == operator tests for deep equality, while in the case of points it
makes a shallow test.

Copying
Aliasing can make a program difficult to read because changes made in one place might have
unexpected effects in another place. It is hard to keep track of all the variables that might refer to
a given object.
Copying an object is often an alternative to aliasing. The copy module contains a function called
copy that can duplicate any object:

>>> import copy


>>> p1 = Point(3, 4)
>>> p2 = [Link](p1)
>>> p1 is p2
False
>>> same_coordinates(p1, p2)
True

[Link]
Python Programming Module 5

Once we import the copy module, we can use the copy function to make a new Point. p1 and p2
are not the same point, but they contain the same data.
To copy a simple object like a Point, which doesn’t contain any embedded objects, copy is
sufficient. This is called shallow copying.

import copy

# Original list with nested list


original = [[1, 2, 3], [4, 5, 6]]
# Shallow copy
shallow_copy = [Link](original)
# Deep copy
deep_copy = [Link](original)
# Modify nested element
original[0][0] = 99

print("Original:", original)
print("Shallow Copy:", shallow_copy)
print("Deep Copy:", deep_copy)

Output:
Original: [[99, 2, 3], [4, 5, 6]]
Shallow Copy: [[99, 2, 3], [4, 5, 6]]
Deep Copy: [[1, 2, 3], [4, 5, 6]]

[Link]
Python Programming Module 5

MyTime
As another example of a user-defined type, we’ll define a class called MyTime that records the
time of day. We’ll provide an init method to ensure that every instance is created with
appropriate attributes and initialization. The class definition looks like this:

class MyTime:
def init (self, hrs=0, mins=0, secs=0):
""" Create a MyTime object initialized to hrs, mins, secs """
[Link] = hrs
[Link] = mins
[Link] = secs

We can instantiate a new MyTime object:


tim1 = MyTime(11,59,30)
The state diagram for the object looks like this:

[Link]
Python Programming Module 5

Pure Functions
We’ll write two versions of a function called add_time, which calculates the sum of two MyTime
objects. They will demonstrate two kinds of functions: pure functions and modifiers
The following is a rough version of add_time:

def add_time(t1, t2):


h = [Link] + [Link]
m = [Link] + [Link]
s = [Link] + [Link]
sum_t = MyTime(h, m, s)
return sum_t

The function creates a new MyTime object and returns a reference to the new object. This is called
a pure function because it does not modify any of the objects passed to it as parameters and it has
no side effects, such as updating global variables, displaying a value, or getting user input.

Here is an example of how to use this function. We’ll create two MyTime objects: current_time,
which contains the current time; and bread_time, which contains the amount of time it takes for a
breadmaker to make bread. Then we’ll use add_time to figure out when the bread will be done

>>> current_time = MyTime(9, 14, 30)


>>> bread_time = MyTime(3, 35, 0)
>>> done_time = add_time(current_time, bread_time)
>>> print(done_time)
12:49:30

The output of this program is 12:49:30, which is correct. On the other hand, there are cases where
the result is not correct. Can you think of one?

The problem is that this function does not deal with cases where the number of seconds or minutes
adds up to more than sixty. When that happens, we have to carry the extra seconds into the minutes
column or the extra minutes into the hours column.

[Link]
Python Programming Module 5

Here’s a better version of the function:

def add_time(t1, t2):

h = [Link] + [Link]
m = [Link] + [Link]
s = [Link] + [Link]

if s >= 60:
s-= 60
m += 1

if m >= 60:
m-= 60
h += 1
sum_t = MyTime(h, m, s)
return sum_t

Modifiers
There are times when it is useful for a function to modify one or more of the objects it gets as
parameters. Usually, the caller keeps a reference to the objects it passes, so any changes the
function makes are visible to the caller. Functions that work this way are called modifiers.

increment, which adds a given number of seconds to a MyTime object, would be written most
naturally as a modifier. A rough draft of the function looks like this:

def increment(t, secs):


[Link] += secs

if [Link] >= 60:


[Link]-= 60
[Link] += 1

if [Link] >= 60:


[Link]-= 60
[Link] += 1
The first line performs the basic operation; the remainder deals with the special cases we saw
before.

[Link]
Python Programming Module 5

Is this function correct? What happens if the parameter seconds is much greater than sixty? In that
case, it is not enough to carry once; we have to keep doing it until seconds is less than sixty. One
solution is to replace the if statements with while statements:

def increment(t, seconds):


[Link] += seconds

while [Link] >= 60:


[Link]-= 60
[Link] += 1

while [Link] >= 60:


[Link]-= 60
[Link] += 1
This function is now correct when seconds is not negative, and when hours does not exceed 23,
but it is not a particularly good solution.

Generalization
In some ways, converting from base 60 to base 10 and back is harder than just dealing with times.
Base conversion is more abstract; our intuition for dealing with times is better.
But if we have the insight to treat times as base 60 numbers and make the investment of writing
the conversions, we get a program that is shorter, easier to read and debug, and more reliable.
It is also easier to add features later. For example, imagine subtracting two MyTime objects to
find the duration between them. The naive approach would be to implement subtraction with
borrowing. Using the conversion functions would be easier and more likely to be correct.

Ironically, sometimes making a problem harder (or more general) makes the programming easier,
because there are fewer special cases and fewer opportunities for error

Specialization versus Generalization


Computer Scientists are generally fond of specializing their types, while mathematicians often take
the opposite approach, and generalize everything.
What do we mean by this? If we ask a mathematician to solve a problem involving weekdays, days
of the century, playing cards, time, or domi noes, their most likely response is to observe that all
these objects can be represented by integers. Playing cards, for example, can be numbered from 0
to 51. Days within the century can be numbered. Mathematicians will say “These things are
enumerable — the elements can be uniquely numbered (and we can reverse this numbering to get

[Link]
Python Programming Module 5

back to the original concept). So let’s number them, and confine our thinking to integers. Luckily,
we have powerful techniques and a good understanding of integers, and so our abstractions — the
way we tackle and simplify these problems — is to try to reduce them to problems about integers.”
Computer Scientists tend to do the opposite. We will argue that there are many integer operations
that are simply not meaningful for dominoes, or for days of the century. So we’ll often define new
specialized types, like MyTime, because we can restrict, control, and specialize the operations that
are possible. Object-oriented programming is particularly popular because it gives us a good way
to bundle methods and specialized data into a new type.
Both approaches are powerful problem-solving techniques. Often it may help to try to think about
the problem from both points of view — “What would happen if I tried to reduce everything to
very few primitive types?”, versus “What would happen if this thing had its own specialized type?

Operator Overloading
Some languages, including Python, make it possible to have different meanings for the same
operator when applied to different types. For example, + in Python means quite different things
for integers and for strings. This feature is called operator overloading.
It is especially useful when programmers can also overload the operators for their own user-defined
types.

For example, to override the addition operator +, we can provide a method named add :

class MyTime:
# Previously defined methods here...

def add (self, other):


return MyTime(0, 0, self.to_seconds() + other.to_seconds())
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. To add two MyTime objects, we create and
return a new MyTime object that contains their sum.
Now, when we apply the + operator to MyTime objects, Python invokes the add method that
we have written:

>>> t1 = MyTime(1, 15, 42)


>>> t2 = MyTime(3, 50, 30)
>>> t3 = t1 + t2
>>> print(t3)
05:06:12
The expression t1 + t2 is equivalent to t1. add (t2), but obviously more elegant.

[Link]
Python Programming Module 5

class Point:
def init (self, x, y):

self.x = x

self.y = y

# Overload + operator
def add (self, other):
return Point(self.x + other.x, self.y + other.y)

def str (self):

return f"({self.x}, {self.y})"

p1 = Point(2, 3)
p2 = Point(4, 5)

print(p1 + p2) # (6, 8)

Here, + adds two Point objects by summing their coordinates.

class Vector:

def init (self, x, y):

self.x = x

self.y = y
def mul (self, scalar):

return Vector(self.x * scalar, self.y * scalar)


def str (self):

return f"Vector({self.x}, {self.y})"

v = Vector(2, 3)

print(v * 3) # Vector(6, 9) Here, The * operator multiplies a vector by a number.

[Link]
Python Programming Module 5

Polymorphism
Polymorphism (from Greek poly = many, morph = forms) in Python refers to the ability of the
same interface (method, operator, or function) to exhibit different behaviors depending on
the object or context.
In other words, the same operation can be applied to objects of different types, and each object
can respond in its own way.

Why It Matters in OOP


 Abstraction & Interfaces: Polymorphism allows us to design systems where code
depends on behavior rather than concrete types.

 Extensibility: New classes can be introduced without modifying existing code, as long as
they follow the expected interface.
 Loose Coupling: Promotes modularity and testability by reducing dependency on specific
implementations.

Types of Polymorphism in Python

1. Duck Typing (Ad-hoc Polymorphism)


Python is dynamically typed, so polymorphism often arises from behavioral compatibility rather
than inheritance.
“If it walks like a duck and quacks like a duck, it’s a duck.”

class Duck:
def sound(self):
return "Quack"

class Dog:
def sound(self):
return "Woof"

for animal in (Duck(), Dog()):


print([Link]())
Both objects respond to sound(), even though they are unrelated classes.

[Link]
Python Programming Module 5

2. Operator Overloading
Operators are syntactic sugar for special methods ( add , eq , etc.).
This allows the same operator to behave differently for different object types.

class Vector:

def init (self, x, y):

self.x, self.y = x, y

def add (self, other):


return Vector(self.x + other.x, self.y + other.y)

def repr (self):


return f"Vector({self.x}, {self.y})"

print(Vector(1, 2) + Vector(3, 4)) # Vector(4, 6)

3. Method Overriding (Runtime Polymorphism)


Subclass methods can override parent methods, and the correct method is chosen at runtime.

class Shape:
def area(self): raise NotImplementedError

class Circle(Shape):
def init (self, r):
self.r = r
def area(self):
return 3.14 * self.r * self.r

class Square(Shape):
def init (self, s):
self.s = s
def area(self):
return self.s * self.s

for shape in (Circle(3), Square(4)):


print([Link]())

[Link]
Python Programming Module 5

4. Function Overloading (Simulated)


Python doesn’t support true compile-time overloading (like C++/Java).
Instead, it uses default arguments or *args/**kwargs to mimic it.

def multiply(a, b=1, c=1):


return a * b * c

print(multiply(2)) #2

print(multiply(2, 3)) #6

print(multiply(2, 3, 4)) # 24

[Link]
Python Programming Module 5

EXCEPTIONS
An exception in Python is an error that occurs during the execution of a program.

 Unlike syntax errors (which stop the program before it runs), exceptions happen at runtime
when something unexpected occurs.

 When an exception is raised, Python stops the normal flow of the program and looks for
code to handle the problem.

Catching Exceptions
Whenever a runtime error occurs, it creates an exception object. The program stops running at this
point and Python prints out the traceback, which ends with a message describing the exception that
occurred.
For example, dividing by zero creates an exception:

>>> print(55/0)
Traceback (most recent call last):
File ""<interactive input>", line 1, in
ZeroDivisionError: integer division or modulo by zero
So does accessing a non-existent list item:

>>> a = [ ]
>>> print(a[5])
Traceback (most recent call last):
File "<interactive input>", line 1, in
IndexError: list index out of range
Or trying to make an item assignment on a tuple:

>>> tup = ("a", "b", "d", "d")


>>> tup[2] = "c"
Traceback (most recent call last):
File ""<interactive input>", line 1, in
TypeError: 'tuple' object does not support item assignment
In each case, the error message on the last line has two parts: the type of error before the colon,
and specifics about the error after the colon.

Sometimes we want to execute an operation that might cause an exception, but we don’t want the
program to stop. We can handle the exception using the try statement to “wrap” a region of code.

For example, we might prompt the user for the name of a file and then try to open it. If the file
doesn’t exist, we don’t want the program to crash; we want to handle the exception:

[Link]
Python Programming Module 5

filename = input("Enter a file name: ")


try:
f = open(filename, "r")
except FileNotFoundError:
print("There is no file named", filename)

The try statement has four separate clauses—or parts—introduced by the keywords try, except,
else, and finally. All clauses but the try can be omitted.

The interpretor executes the block under the try statement, and monitors for exceptions. If one
occurs, the interpretor moves to the except statement; it executes the expect block if the exception
raised match the exception requested in the except statement. If no exception occurs, the interpretor
skips the block under the except clause. A else block is executed after the try one, if no exception
occurred. A finally block is executed in any case. With all the statements, a try clause looks like:

user_input = input('Type a number:')


try:
# Try do do something that could fail.
user_input_as_number = float(user_input)
except ValueError:
# This will be executed if a ``ValueError`` is raised.
print('You did not enter a number.')
else:
# This will be executed if not exception got raised in the
# ``try`` statement.
print('The square of your number is ', user_input_as_number**2)
finally:
# This will be executed whether or not an exception is raised.
print('Thank you')
When using a try clause, you should have as little as possible in the try block. If too many things
happen in that block, you risk handling an unexpected exception.

If the try block can fail if various way, you can handle different exceptions in the same try clause:

try:
with open(filename) as infile:
content = [Link]()
except FileNotFoundError:
print('The file does not exist.')
except PermissionError:
print('Your are not allowed to read this file.')

[Link]
Python Programming Module 5

Raising our own exceptions


Can our program deliberately cause its own exceptions? If our program detects an error condition,
we can raise an exception. Here is an example that gets input from the user and checks that the
number is non-negative:

def get_age():
age = int(input("Please enter your age: "))
if age < 0:
# Create a new instance of an exception
my_error = ValueError("{0} is not a valid age".format(age))
raise my_error
return age

Line 5 creates an exception object, in this case, a ValueError object, which encapsulates specific
information about the error. Assume that in this case function A called B which called C which
called D which called get_age. The raise statement on line 6 carries this object out as a kind of
“return value”, and immediately exits from get_age() to its caller D. Then D again exits to its caller
C, and C exits to B and so on, each returning the exception object to their caller, until it encounters
a try ... except that can handle the exception. We call this “unwinding the call stack”.

ValueError is one of the built-in exception types which most closely matches the kind of error we
want to raise. The complete listing of built-in exceptions can be found at the Built-in Exceptions
section of the Python Library Reference , again by Python’s creator, Guido van Rossum.
If the function that called get_age (or its caller, or their caller, ...) handles the error, then the
program can carry on running; otherwise, Python prints the traceback and exits:

>>> get_age()
Please enter your age: 42
42
>>> get_age()
Please enter your age:-2
Traceback (most recent call last):
File "<interactive input>", line 1, in
File "learn_exceptions.py", line 4, in get_age
raise ValueError("{0} is not a valid age".format(age))
ValueError:-2 is not a valid age
The error message includes the exception type and the additional information that was provided
when the exception object was first created.

[Link]
Python Programming Module 5

It is often the case that lines 5 and 6 (creating the exception object, then raising the exception) are
combined into a single statement, but there are really two different and independent things
happening, so perhaps it makes sense to keep the two steps separate when we first learn to work
with exceptions. Here we show it all in a single statement:

raise ValueError("{0} is not a valid age".format(age))

[Link]

You might also like