MODULE – 5
Classes and objects: Programmer-defined types, Attributes, Rectangles,
Instances as return values, Objects are mutable, Copying,
Classes and functions: Time, Pure functions, Modifiers, Prototyping versus
planning,
Classes and methods: Object-oriented features, Printing objects, Another
example, A more complicated example, The init method, The __str__ method,
Operator overloading, Type-based dispatch, Polymorphism, Interface and
implementation
what is class? how to define a class in Python? How to initiate a class and how
the class members are accessed?
What is a Class?
• In Python, a class is a blueprint for creating objects (instances).
• A class defines the properties (attributes) and behaviours (methods)
that its instances(object) will have.
Defining a Class in Python
• To define a class, use the “class” keyword followed by the name of the
class (typically written in PascalCase).
syntax:
class ClassName:
def __init__(self, attribute1, attribute2):
self.attribute1 = attribute1
self.attribute2 = attribute2
def method_name(self):
# Code for the method
pass
• __init__ is the constructor method. It is automatically called when you
create an object (instance) of the class. This method is used to initialize
the attributes of the class.
• self refers to the current instance(object) of the class.
• It is used to access attributes that belong to the class and methods within
the class.
Example:
class Car:
def __init__(self, brand, model, year):
self.brand = brand
self.model = model
self.year = year
def display_info(self):
print(f"This car is a {self.year} {self.brand} {self.model}.")
my_car = Car("Toyota", "Corolla", 2020)
In this example:
• Car is the class.
• The __init__ method initializes the class with three attributes: brand,
model, and year.
• display_info is a method that displays information about the car.
Creating an Object (Instantiating a Class)
Once you have defined a class, you can create instances (objects) of that class
by calling the class as if it were a function, passing the required arguments to
the __init__ method.
object_name = class name()# (arguments)
my_car = Car("Toyota", "Corolla", 2020) #
In this example, my_car is an instance of the Car class, with the brand
"Toyota", model "Corolla", and year 2020.
Accessing Class Members
There are two types of class members:
1. Attributes: These are the data values associated with an instance of the
class.
2. Methods: These are functions that define behaviours or actions related to
the instance of the class.
Accessing Attributes
You can access the attributes of an instance using the dot (.) notation:
print(my_car.brand) # Output: Toyota
print(my_car.model) # Output: Corolla
print(my_car.year) # Output: 2020
Accessing Methods
You can also call methods using the dot notation:
my_car.display_info() # Output: This car is a 2020 Toyota Corolla.
3. Rectangles (A Class Example)
Example: Define a Rectangle Class
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
This creates a class with two attributes:
• width
• height
Each Rectangle object will have its own width and height.
Creating Rectangle Objects:
r1 = Rectangle(10, 5)
r2 = Rectangle(4, 7)
Here,
• r1.width = 10, r1.height = 5
• r2.width = 4, r2.height = 7
Add Behavior: Methods (e.g., area, perimeter)
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
Usage of Methods:
r1 = Rectangle(10, 5)
print("Area:", r1.area()) # Output: 50
print("Perimeter:", r1.perimeter()) # Output: 30
Summary Table:
Component Meaning
__init__() Initializes new objects
self.width Object-specific attribute
area() Method to calculate area using attributes
r1.area() Call method on object r1
Instances as Return Values
What Does This Mean?
In Python, a function can return an object (instance) of a class.
This is very useful when:
• You want to create objects inside a function.
• You want to hide complexity and return a ready-to-use object.
Example:
Let’s extend the Rectangle class from earlier.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
Now, define a function that returns a Rectangle object:
def create_square(side_length):
return Rectangle(side_length, side_length)
Usage:
sq = create_square(6)
print("Square area:", sq.area()) # Output: 36
Here:
• create_square() is a function that returns a Rectangle instance.
• sq is now an object of the Rectangle class.
• It behaves like any other rectangle.
Benefit Explanation
Can reuse the same function to create many
Code reuse
instances
Cleaner design User doesn't need to know class details
Can return different Helpful in advanced object-oriented
subclasses programming
Another Example: Returning a Student Object
class Student:
def __init__(self, name, roll_no):
self.name = name
self.roll_no = roll_no
def create_student():
return Student("Hithesh", 101)
s1 = create_student()
print(s1.name) # Output: Hithesh
Key Takeaways:
Concept Description
Function returns instance Using return ClassName(...)
Access like any object After return, use dot notation normally
Cleaner code Makes object creation reusable and modular
Objects are Mutable
What Does Mutable Mean?
Mutable means changeable.
If an object is mutable, you can modify its attributes after it has been
created.
In Python, most built-in types like list, dict, and custom class instances (like
Student, Rectangle) are mutable.
Example:
Let’s use our Rectangle class again:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
Now create and modify an object:
r1 = Rectangle(10, 5)
print("Area:", r1.area()) # Output: 50
# Change attributes
r1.width = 20
print("New Area:", r1.area()) # Output: 100
• We changed the width from 10 to 20.
• The object remembers the change, and it affects future behavior.
Why Does Mutability Matter?
1. Efficient Memory Use:
o Instead of creating a new object, we can update the existing one.
2. Function Behavior:
o If a function modifies an object passed to it, the original object will
change.
Summary Table:
Term Meaning
Mutable Can be changed after creation
Implies Attribute values can be updated
Impact Changes inside a function affect the object
Examples Classes, lists, dictionaries (mutable)
Non-mutable int, str, tuple (these can't change)
Copying Objects
What Does It Mean?
Copying an object means creating a new object with the same attributes or
data as an existing one.
But there are two ways this can happen in Python:
Type of Copy Description
Shallow Copy Copies the reference (pointer), not actual data
Deep Copy Copies everything (a completely new object)
Default Assignment is Not Copying
r1 = Rectangle(10, 5)
r2 = r1 # Not a real copy!
r2.width = 20
print(r1.width) # Output: 20 (because r1 and r2 are same object)
• r2 is just another name for the same object.
• Any change to r2 affects r1.
Real Copy Using copy Module
import copy
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
r1 = Rectangle(10, 5)
r2 = copy.copy(r1) # Shallow copy
r2.width = 20
print("r1.width:", r1.width) # Output: 10
print("r2.width:", r2.width) # Output: 20
• r2 is a new object.
• Changes to r2 do not affect r1.
Deep Copy Example
Deep copy is used when your object contains other mutable objects inside it
(like lists).
class Box:
def __init__(self, items):
self.items = items
b1 = Box(["apple", "banana"])
b2 = copy.deepcopy(b1)
b2.items.append("cherry")
print(b1.items) # Output: ['apple', 'banana']
print(b2.items) # Output: ['apple', 'banana', 'cherry']
Without deepcopy(), both boxes would share the same list.
Key Differences:
Copy Type Description When to Use
Just another name for the
Assignment Never for true copies
same object
New object, but shallow
copy.copy() Safe for simple objects
inside
Needed if object contains lists
copy.deepcopy() Fully independent copy
or other objects
(Imagine photocopying a form:
• A shallow copy is like sharing the same form with someone.
• A deep copy is like making a new copy, and each person writes
independently.)
Summary:
• Use the copy module when you want to duplicate objects.
• Shallow copy is fine for simple data.
• Deep copy is important when objects contain other objects inside them.
_ _init_ _ method
➢ In Python, the `__init__` method is a special method used for initializing
newly created objects.
➢ It's called a constructor method because it initializes the object when it
is created.
➢ The __init__ method is defined within a class and is automatically
called when a new object of the class is created.
example:
class MyClass:
def _ _init_ _ (self, x, y):
self.x = x
self.y = y
# Creating an object/instance of MyClass
obj = MyClass(10, 20)
print(obj.x)
print(obj.y)
Output: 10
Output: 20
In this example, the `__init__` method takes three parameters: `self`, `x`, and
`y`.
`self` refers to the instance/object itself and is automatically passed when the
method is called.
The `x` and `y` parameters are used to initialize the instance variables `x` and
`y` respectively.
When we create an object of `MyClass` (`obj`), the `__init__` method is
automatically called with the arguments `(10, 20)`, so `obj.x` is set to `10` and
`obj.y` is set to `20`.
The `__init__` method can be used to perform any necessary initialization tasks,
such as initializing instance variables, setting up default values, or performing
any other setup actions required when an object is created.
The _ _ str_ _ method
In Python, the `__str__` method is a special method used to define the string
representation of an object.
When you use built-in functions like `str()` or `print()` on an object, Python
calls the object's `__str__` method to get its string representation.
Here's a simple example to illustrate how `__str__` works:
class MyClass:
def _ _init_ _(self, x, y):
self.x = x
self.y = y
def _ _str_ _(self):
return f"MyClass: x={self.x}, y={self.y}"
# Creating an object of MyClass
obj = MyClass(10, 20)
# Using print() on the object
print(obj)
Output:
MyClass: x=10, y=20
In this example, the `_ _str_ _` method is defined within the `MyClass` class .
It returns a string that represents the state of the object.
When `print(obj)` is called, Python internally calls `obj._ _str_ _()` to get the
string representation of the object, which is then printed.
You can customize the string representation returned by `__str__` to provide
any information about the object that you find useful. This method is often used
to provide a human-readable representation of objects, making debugging and
understanding code easier.
Operator overloading
Operator overloading in Python refers to the ability to define custom behaviour
for operators when they are used with user-defined objects.
This means that you can redefine the meaning of operators such as `+`, `-`, `*`,
`/`, `==`, `!=`, `<`, `>`, and many others for your own classes.
For example, you can define what it means to add two instances of a custom
class together, or how to compare them for equality.
Here's a simple example demonstrating operator overloading in Python:
Example:
class Point:
def _ _init_ _(self, x, y):
self.x = x
self.y = y
def _ _add_ _ (self, other):
# Define addition behaviour for Point objects
return Point(self.x + other.x, self.y + other.y)
def _ _str_ _(self):
# Define string representation for Point objects
return f"({self.x}, {self.y})"
# Creating two Point objects
point1 = Point(1, 2)
point2 = Point(3, 4)
# Adding two Point objects
result = point1 + point2 # it will call __add__ method
print(result) # it will call __str__ method
# Output: (4, 6)
In this example:
- The `_ _add_ _` method is defined to override the behavior of the `+` operator
for `Point` objects.
When two `Point` objects are added together using the `+` operator (`point1 +
point2`), the `_ _add_ _` method is called, and the result is a new `Point` object
with coordinates that are the sum of the corresponding coordinates of the
operands.
- The `__str__` method is defined to provide a string representation of `Point`
objects when they are printed or converted to a string.
By implementing these special methods, you can define custom behavior for
operators in your classes, allowing them to behave naturally in Python
expressions and operations.
Inheritance
➢ Inheritance is a fundamental concept in OOP that allows us to create a
new class based on an existing class.
➢ The new class inherits attributes and behaviours from the existing
class, and it can also add its unique attributes and behaviours.
➢ This promotes code reuse and allows for building a hierarchy of
classes, which is particularly useful when modelling objects that share
common characteristics.
Eg:
# Super class/Parent class
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print("{} makes a sound".format(self.name))
# Sub class/Child class (inherits from Animal)
class Dog(Animal):
def speak(self):
print("{} barks".format(self.name))
# Creating an object of the child class
dog1 = Dog("Tommy")
dog1.speak()
Output:
Tommy barks
Explanation:
1. Class Animal:
o This is the parent class (also called the base class).
o It has:
▪ An __init__() method (constructor) that accepts name and
assigns it to self.name.
▪ A method speak() that prints:
"Tommy makes a sound" using .format().
2. Class Dog:
o This is a child class, created by inheriting from Animal using class
Dog(Animal):.
o It inherits the __init__() method from Animal, so you don’t have to
write it again.
o It overrides the speak() method to provide a specific message:
"Tommy barks".
3. Object Creation:
o dog1 = Dog("Tommy") creates an object dog1 of class Dog.
o "Tommy" is passed to the constructor, which sets self.name =
"Tommy".
4. Method Call:
o dog1.speak() calls the speak() method from the Dog class (not the
one in Animal).
o So, it prints: Tommy barks.
Inheritance promotes code reuse and simplifies the design of your program by
allowing you to create specialized classes that inherit common attributes and
behaviors from a base class. This makes your code more maintainable and
reduces redundancy.
Polymorphism
Polymorphism is a fundamental concept in object-oriented programming (OOP)
that refers to the ability of different objects to respond to the same message
or method call in different ways.
It allows functions, methods or operators to behave differently based on the
type of data they are handling. Derived from Greek, Polymorphism means
“Many forms”.
In Python, polymorphism is achieved through method overriding and method
overloading.
1. Method Overriding:
When a subclass provides a specific implementation of a method that is
already defined in its superclass, it is called method overriding. The method
in the subclass "overrides" the implementation of the method in the
superclass. When the method is called on an instance of the subclass, the
overridden method in the subclass is executed.
Example:
class Animal: # super class
def speak(self):
print("Animal speaks")
class Dog(Animal): # subclass
def speak(self):
print("Dog barks")
# Creating instances
animal = Animal()
dog = Dog()
# Polymorphism
animal.speak()
Output: Animal speaks
dog.speak()
Output: Dog barks
2. Method Overloading:
In Python, method overloading is not directly supported as it is in some other
languages like Java. However, you can achieve a form of method
overloading using default arguments or variable-length arguments.
Default arguments:
This allows a single method to behave differently based on the number
or type of arguments passed to it.
Example using default arguments:
class Calculator:
def add(self, a, b=0):
return a + b
calc = Calculator()
print(calc.add(5))
Output: 5
print(calc.add(2, 3))
Output: 5
Example using variable-length arguments:
class Calculator:
def add(self, *args): # arbitrary arguments
return sum(args)
calc = Calculator()
print(calc.add(5))
Output: 5
print(calc.add(2, 3))
Output: 5
print(calc.add(1, 2, 3, 4))
Output: 10
In both examples, the `add` method behaves differently based on the number
of arguments passed to it, demonstrating polymorphic behaviour.
Polymorphism helps in writing flexible and reusable code by allowing different
objects to be treated uniformly through a common interface, even though they
may behave differently. This promotes code reuse and simplifies the design of
software systems.
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
t = ['spam', 'egg', 'spam', 'spam', 'bacon', 'spam']
histogram(t)
Type-Based Dispatch
Type-based dispatch in Python is a way to change how a function behaves
based on the type of input it receives.
It's useful in data science, where different input types might need to be
processed by the same function.
Working.
• Type-based dispatch maps types from type annotations to functions.
• When a function is called with input, the type-based dispatch system
determines which function to call based on the input type.
• This ensures that the correct function is called for each input.
• You can use the TypeDispatch class in Python to implement type-based
dispatch.
• You can also use the @overload annotation to support multiple dispatch.
• Type-based dispatch can improve documentation and avoid code
repetition.
• It can create a common API for functions that perform similar tasks.
Example:
def process(value):
if isinstance(value, int):
return f"Integer: {value * 2}"
elif isinstance(value, str):
return f"String: {value[::-1]}" # Reverses the string
elif isinstance(value, list):
return f"List: {[item * 2 for item in value]}"
else:
return "Unsupported type!"
# Test cases
print(process(10)) # Output: Integer: 20
print(process("hello")) # Output: String: olleh
print(process([1, 2, 3])) # Output: List: [2, 4, 6]
print(process(3.14)) # Output: Unsupported type!
The function uses isinstance to check the type of the input argument.
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.
Interface defines the "what" (expected methods and behaviours).
Implementation defines the "how" (actual logic and functionality).
What Is an Interface?
An interface defines what an object can do — not how it does it.
What Is Implementation?
Implementation is the actual code or logic written inside the class methods to
fulfill the interface.
Concept Real-world Analogy
Interface A TV remote's buttons (what user sees/uses)
Implementation Internal circuit of the TV (how it works)
The user presses a button (interface), not knowing how signals are sent
(implementation).
Python Example:
Let’s create a class that behaves like a shape (e.g., rectangle or circle):
class Shape:
def area(self):
pass # interface method
def perimeter(self):
pass # interface method
This class defines the interface (a set of method names that other classes must
follow).
Now, let’s implement it:
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
Using Interface and Implementation:
def print_shape_info(shape):
print("Area:", shape.area())
print("Perimeter:", shape.perimeter())
rect = Rectangle(5, 4)
print_shape_info(rect)
• print_shape_info() works with any object that implements the shape
interface.
• We don’t care if it’s a Rectangle, Circle, or Hexagon.
• As long as it has .area() and .perimeter() methods, it works.
Aspect Interface Implementation
Definition Specifies the methods a Provides the actual
class must implement. functionality of the
methods.
Purpose Defines what actions are Defines how those
expected. actions are performed.
Focus Focuses on abstraction. Focuses on concrete
behavior.
Reusability Promotes code Implements specific
reusability and logic for different use
consistency. cases.
Keeping the interface separate from the implementation means that you have to
hide the attributes.
Code in other parts of the program (outside the class definition) should use
methods to read and modify the state of the object.
They should not access the attributes directly. This principle is called
information hiding;
Pure functions and Modifiers.
Pure functions
A pure function is a function that satisfies two key properties:
• The function always produces the same output for the same input.
• The function does not modify any external state or variable outside its
scope.
In simple terms, a pure function always returns the same result when called with
the same arguments and does not alter any external variables or state (such as
global variables, attributes).
Example of Pure Function
class Time:
def __init__(self, hours, minutes):
self.hours = hours
self.minutes = minutes
def total_minutes(self):
return self.hours * 60 + self.minutes
total_minutes() is pure because:
• It doesn't change the object
• It just returns a computed result
Advantages of Pure Functions:
• Easier to test and debug.
• Can be parallelized, as they don’t rely on shared mutable state.
• Enhances code readability and maintainability.
Modifiers
In Python, modifiers generally refer to methods that modify the state of an
object or class.
These are methods that change the value of an object's attributes or perform
operations that alter the state.
They are commonly referred to as "mutators" or "setters" when they modify
instance attributes.
Example
class Time:
def __init__(self, hours, minutes):
self.hours = hours
self.minutes = minutes
def add_minutes(self, mins):
self.minutes += mins
while self.minutes >= 60:
self.hours += 1
self.minutes -= 60
add_minutes() is a modifier
It changes the state of the object (updates hours and minutes)
Prototyping vs Planning
Prototyping and Planning are two distinct approaches to software
development, each with its own characteristics, benefits, and challenges.
Definition
• Prototyping:
o Involves creating a working model (prototype) of the software
system early in the development process.
o This model is used to gather feedback and improve the design
iteratively.
o It is often used when the requirements are unclear or
changing/evolving.
• Planning:
o Refers to creating a detailed and comprehensive plan before
starting the development process.
o It includes defining the project scope, timelines, resources, and
requirements in advance, aiming to have a clear and fixed vision of
the end product before starting work.
Aspect Prototyping Planning
Creating a preliminary version Outlining the detailed steps,
Definition or model of a product to explore objectives, and resources for
ideas or test functionality. executing a project.
Aspect Prototyping Planning
To experiment, validate ideas, To establish a structured
Purpose and identify potential issues roadmap to achieve the
early in development. project's goals efficiently.
Emphasis on organization,
Emphasis on experimentation
Focus resource allocation, and
and iterative improvement.
setting clear goals.
Early stages of development or Before execution or as an
Stage of Use when exploring solutions to a ongoing process to guide the
problem. project.
High level of detail,
Minimal detail; focuses on including timelines,
Level of Detail
functionality or core features. milestones, and resource
management.
Project management tools,
Sketches, wireframes, mock-
Tools Used Gantt charts, budgets, and
ups, or working models.
documentation.
Less flexible; changes
Highly flexible; allows changes
Flexibility require updates to plans and
and experimentation.
timelines.
A tangible representation or A comprehensive plan that
Outcome proof of concept to validate serves as a blueprint for
ideas. execution.
Focused on aligning
Encourages feedback and
Stakeholder expectations and
collaboration from stakeholders
Involvement responsibilities before
during iterations.
project execution.
Aspect Prototyping Planning
Short-term; used to quickly Long-term; outlines the full
Timeframe
explore and refine ideas. duration of the project.
• Both approaches often complement each other in product development.
• Prototyping can inform and refine planning, while planning ensures the
prototype fits into the larger project goals.