Module 5 Python 1BPLC205B
Module 5 Python 1BPLC205B
Syllabus
1
Page
We can prove an object is mutable by checking its unique memory address using Python's
built-in id() function. If we modify an attribute, the data changes, but the id() stays exactly
the same.
Example:
class Account:
def __init__(self, owner, balance):
[Link] = owner
[Link] = balance
print("-" * 30)
Sameness In Python
When talking about sameness in Python, it means understanding the difference between
two concepts that sound identical but mean completely different things in memory:
1. Equality (==): Do these two objects contain the same data? (Value)
2. Identity (is): Are these two variables pointing to the exact same object in memory?
(Location)
This distinction is crucial because custom objects are mutable. Two objects can look
exactly the same on the outside while being completely different entities under the hood.
NOTE :
Use == when you care about the content (e.g., checking if a user entered the correct
password or if two vectors have the same coordinates).
Use is when you care about the exact object identity. In practice, is is most commonly
used when checking if something is None (e.g., if value is None:).
print([Link])
print([Link])
3
obj = Rectangle()
[Link](11,22)
[Link](33,44)
OUTPUT
11
22
33
44
Copying In Python
Because Python custom objects are mutable and variables only store
references (pointers) to objects in memory, copying is not as simple as writing
object2 = object1.
If you want to duplicate an object so that changes to the new one don't ruin
the original, you need to use Python’s built-in copy module. There are two
ways to do this: Shallow Copy and Deep Copy.
1. Shallow Copy ([Link]())
A shallow copy creates a new outer object, but if that object contains nested
mutable objects (like a list inside a class), it copy-pastes the references to
those nested objects rather than duplicating them.
2. Deep Copy ([Link]())
A deep copy completely duplicates everything. It creates a new outer object
and recursively clones every single nested object inside it. The two objects
become 100% independent.
4
Page
INHERITANCE
Pure Function
A function is called pure function if it always returns the same result for same
argument values and it has no side effects like modifying an argument (or
global variable) or outputting something. The only result of calling a pure
function is the return value. Examples of pure functions are strlen(), pow(),
sqrt() etc. Examples of impure functions are printf(), rand(), time(), etc.
Advantages Of Pure Function
• Predictable: Same inputs always give the same output.
• Easy to test: No external setup or cleanup needed.
• Bug-free concurrency: Safe to run on multiple threads.
• Cacheable: Results can be stored to speed up code.
• Reusable: Completely independent and easy to move around.
• Easier to read: Everything the function does is visible.
Pure Function Example
The following function is pure because it only depends on its inputs and does
not change anything outside itself.
Example:
def multiply(x, y):
return x * y
NOTE : It relies solely on its parameters x and y and does nothing except
return their product.
Modifiers in Python
The Python access modifiers are used to restrict access to class members
(i.e., variables and methods) from outside the class.
There are three types of access modifiers namely
• Public members − A class member is said to be public if it can be
5
Example:
class Employee:
def __init__(self, name, age, salary):
[Link] = name # Public: accessible anywhere
self._age = age # Protected: accessible in subclasses
self.__salary = salary # Private: accessible only in Employee class
class Manager(Employee):
def show_details(self):
# Can access public and protected attributes
print(f"Name: {[Link]}, Age: {self._age}")
# 1. Accessing Public
print([Link]) # Output: Alice (Works fine)
# 2. Accessing Protected
print(emp._age) # Output: 30 (Works, but breaks convention)
# 3. Accessing Private
# print(emp.__salary) # Error! AttributeError
Generalization in Python
Generalization in Object-Oriented Programming (OOP) is the process of
extracting shared features (attributes and methods) from specific classes and
combining them into a broader, more abstract superclass (parent class).
In Python, generalization is implemented directly through inheritance. It
allows you to write reusable code by moving common behaviours up to a base
class, while specific behaviours remain in the subclasses (child classes).
Example:
# Superclass (Generalized Class)
class Vehicle:
def __init__(self, brand, model):
[Link] = brand
[Link] = model
def start_engine(self):
return f"The engine of the {[Link]} {[Link]} is now running."
def open_trunk(self):
# Both objects use the generalized method from the Parent class
print(my_car.start_engine()) # Output: The engine of the Toyota Corolla is
now running.
print(my_bike.start_engine()) # Output: The engine of the Yamaha R1 is now
running.
Operator Overloading
Operator Overloading allows you to redefine how built-in operators
(like +, -, *, <, ==) behave when used with your custom objects.
In Python, this is achieved by overriding special, predefined methods
called magic methods (or dunder methods), which always begin and end
with double underscores (e.g., __add__).
- __sub__(self, other)
* __mul__(self, other)
== __eq__(self, other)
Rules to Remember
• No New Operators: You cannot create entirely new operators
(like **~**); you can only overload existing ones.
• The other Parameter: The magic method takes self (the object on the
left) and other (the object on the right).
• Polymorphism: Operator overloading is a core form of polymorphism,
allowing a single operator interface to handle different data types.
Example:
class Number:
def __init__(self, value):
[Link] = value
def __str__(self):
return str([Link])
Polymorphism in Python
Polymorphism means "many forms". It refers to the ability of an entity (like
a function or object) to perform different actions based on the context.
Technically, Polymorphism allows same method, function or operator to
behave differently depending on object it is working with. This makes code
more flexible and reusable.
Types of Polymorphism
Polymorphism refers to ability of the same method or operation to behave
differently based on object or context. It mainly includes
• Compile-time
• Runtime polymorphism.
1. Compile-time Polymorphism
Compile-time polymorphism means deciding which method or operation to
run during compilation, usually through method or operator overloading.
Languages like Java or C++ support this. But Python doesn’t because it’s
dynamically typed it resolves method calls at runtime, not during
compilation. So, true method overloading isn’t supported, though similar
behavior can be achieved using default or variable arguments.
Example: This code demonstrates method overloading using default and
10
class Calculator:
def multiply(self, a=1, b=1, *args):
result = a * b
for num in args:
result *= num
return result
# Create object
calc = Calculator()
Output
1
4
6
24
class Calculator:
# Create object
calc = Calculator()
EXCEPTION HANDLING
Example: This code attempts division and handles errors gracefully using
try-except-else-finally.
try:
n=0
res = 100 / n
except ZeroDivisionError:
print("You can't divide by zero!")
13
Page
except ValueError:
print("Enter a valid number!")
else:
print("Result is", res)
finally:
print("Execution complete.")
Catching Exceptions
We can handle errors more efficiently by specifying the types of exceptions
we expect. This can make code both safer and easier to debug.
1. Catching Specific Exceptions
Catching specific exceptions makes code to respond to different exception
types differently. It precisely makes your code safer and easier to debug. It
avoids masking bugs by only reacting to the exact problems you expect.
Example: This code handles ValueError and ZeroDivisionError with
different messages.
try:
# This will cause ValueError
x = int("str")
inv = 1 / x # Inverse calculation
except ValueError:
print("Not Valid!")
except ZeroDivisionError:
print("Zero has no inverse!")
Output
Not Valid!
14
Page
Raising an Exceptions
You can raise exceptions in several ways by using the raise statement. The
general syntax for the raise statement is as follows.
Syntax
raise [Exception [, args [, traceback]]]
Here, Exception is the type of exception (for example, NameError)
and argument is a value for the exception argument. The argument is
optional; if not supplied, the exception argument is None.
The final argument, trace back, is also optional (and rarely used in practice),
and if present, is the traceback object used for the exception.
Example
An exception can be a string, a class or an object. Most of the exceptions that
the Python core raises are classes, with an argument that is an instance of the
class. Defining new exceptions is quite easy and can be done as follows –
Syntax
def functionName( level ):
if level < 1:
raise "Invalid level!", level
# The code below to this would not be executed
# if we raise the exception
Note: In order to catch an exception, an "except" clause must refer to the same
exception thrown either class object or simple string. For example, to capture
above exception, we must write the except clause as follows −
try:
Business Logic here...
except "Invalid level!":
Exception handling here...
else:
15
User-Defined Exceptions