0% found this document useful (0 votes)
5 views31 pages

Lab Manual 112

The document is a lab manual for CS-112L Object Oriented Programming, covering foundational Python concepts such as variables, data types, control flow, and memory management. It includes practical lab activities to reinforce learning, focusing on topics like functions, parameters, and higher-order functions. Each section provides examples and syntax to illustrate key programming principles.

Uploaded by

ibrahim2201066
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)
5 views31 pages

Lab Manual 112

The document is a lab manual for CS-112L Object Oriented Programming, covering foundational Python concepts such as variables, data types, control flow, and memory management. It includes practical lab activities to reinforce learning, focusing on topics like functions, parameters, and higher-order functions. Each section provides examples and syntax to illustrate key programming principles.

Uploaded by

ibrahim2201066
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

CS-112L Object Oriented Programing Lab

Lab Manual

Prepared By:

Engr. Hafsa Nayyab


Lab#01 Review of Python Foundations

1. Variables and Dynamic Typing


Variables are memory locations used to store data values during program execution. Python follows dynamic
typing which means that the data type of a variable is determined at runtime instead of declaration time. A
variable can store different types of values during execution which makes Python flexible and easy to use for
beginners.

A variable is used to hold a value that can be modified during execution. It works by assigning data using the
assignment operator (=). The interpreter automatically detects the type of value assigned. Syntax: x = 10

x = 10
print(type(x))
x = "Python"
print(type(x))

In this example, the variable x first stores an integer and then stores a string value. This shows that Python
allows changing variable types dynamically during execution.

1.1 Basic Data Types


Data types define the type of data that a variable can store such as numbers, text, or logical values. Python
provides built-in data types including integers, floats, strings, and booleans. These data types are used to
perform operations and control the flow of programs.

Data types represent the nature of stored values and determine the operations that can be performed. The
interpreter assigns the data type based on the assigned value.

Syntax: num = 10

Example:

num = 25
temp = 36.5
text = "Hello"
status = True
print(type(num))

Here different variables are storing integer, float, string and boolean values. The type() function is used to
identify the data type of stored values.

1.2 Lists
Lists are used to store multiple values in a single variable in an ordered form. They are mutable which means
their elements can be modified during execution. Lists allow duplicate values and can store multiple data types.
A list works by storing elements inside square brackets separated by commas. Elements can be accessed using
indexing starting from zero.

Syntax: my_list = [1,2,3]

Example:

numbers = [10,20,30]
[Link](40)
[Link](20)
print(numbers)

The append() function adds an element to the list while remove() deletes a specified element. The updated list
is displayed using print().

1.3 Tuples
Tuples are ordered collections of elements similar to lists but they are immutable. Once created, tuple values
cannot be changed during execution. Tuples are useful for storing constant data.

A tuple works by storing values inside parentheses separated by commas. Elements can be accessed using
indexing but cannot be modified.

Syntax: my_tuple = (1,2,3)

Example

coords = (33.5,72.9)
print(coords[0])

In this example, a tuple is created to store coordinates. The first value is accessed using index position 0.

1.4 Dictionaries
Dictionaries are used to store data in key-value pairs. Each key must be unique and is used to access its
corresponding value efficiently. Dictionaries are widely used for storing structured data.

A dictionary works by storing keys and values inside curly brackets separated by colons. Values are accessed
using their keys.

Syntax: student = {'name':'Ali'}

Example:

student = {"name":"Ali","age":20}
print(student["name"])

The dictionary stores name and age as key-value pairs. The value associated with key 'name' is accessed and
printed.
1.5 Sets
Sets are unordered collections that store unique values only. Duplicate values are automatically removed from
the set. Sets are useful in operations involving unique data.

A set works by storing elements inside curly brackets. Duplicate values are automatically filtered out.

Syntax: my_set = {1,2,3}

Example:

values = {1,2,2,3}
print(values)

The duplicate value 2 is automatically removed when stored in the set. Only unique values are displayed.

2. Control Flow
Control flow determines the execution path of a program based on conditions and repetition. It allows a
program to make decisions and repeat tasks until certain conditions are satisfied. Python provides conditional
statements and loops to control execution logically.

2.1 Conditional Statements


Conditional statements are used to execute different code blocks based on specific conditions. These
statements help in decision making and logical branching in programs.

The if-else statement checks whether a condition is true or false and executes the respective block.

Syntax: if condition:

marks = 80
if marks>=50:
print("Pass")
else:
print("Fail")

The program checks whether marks are greater than or equal to 50. If true, it prints Pass otherwise Fail.

2.2 For Loop


A for loop is used to iterate over sequences such as lists, strings or ranges. It is commonly used when the
number of iterations is known beforehand.

It works by taking each element from the sequence one by one and executing the block of code.

Syntax: for i in range(5):

for i in range(1,6):
print(i)

The loop prints numbers from 1 to 5 by iterating over the given range.
2.3 While Loop
A while loop is used to execute a block of code repeatedly until the condition becomes false. It is useful when
the number of iterations is unknown.

It works by checking the condition before each iteration and executes the loop until the condition becomes
false.

Syntax: while condition:

count = 1
while count<=5:
print(count)
count+=1

The loop continues printing numbers until the condition becomes false.

2.4 Break and Continue


Break and continue statements are used to control loop execution. Break terminates the loop immediately
while continue skips the current iteration.

They are used in filtering or searching tasks within loops.

Syntax: break / continue

for i in range(1,6):
if i==3:
continue
if i==5:
break
print(i)

The continue skips number 3 while break stops execution when value becomes 5.

2.5 Problem Solving Patterns


Problem solving involves analyzing input data and applying logic to produce required output. In Python, loops
and conditions are used to process numeric and textual data efficiently.

2.5.1 Numeric Processing


Numeric processing includes performing calculations such as sum or average of values. These operations are
commonly required in data analysis problems.

It works by applying arithmetic operations on numeric values. Syntax: values = [1,2,3]

values = [10,20,30]
avg = sum(values)/len(values)
print(avg)

The average of list values is calculated using built-in functions sum() and len().
2.5.2 String Processing
String processing involves manipulating text data using built-in functions. It is useful for validation and
formatting tasks.

It works by applying string methods on text values. Syntax: text = 'Hello'

text = "Python"
print([Link]())

The string is converted to lowercase using the lower() function.

Lab Activities
Write a program to check whether a number is even or odd.
Write a program to find sum of list values.
Write a program to print multiplication table using loop.
Write a program to remove duplicate values from list using set.

Lab #02 – Memory Model, References, Mutability & Immutability

Python Execution & Memory Model


Python manages execution using a memory model where variables store references to objects instead of actual
values. Each object is created in memory and assigned a unique identity. Understanding how names bind to
objects helps in predicting program behavior especially when multiple variables reference the same object.

2.1 Names, Objects and Bindings


In Python, a variable name is a reference to an object stored in memory. Binding occurs when a name is
assigned to an object. Multiple names can bind to the same object in memory.
Syntax: a = 10
a = 10
b = a
print(id(a), id(b))

Both a and b refer to the same object in memory as shown by identical ids. This demonstrates binding of
multiple names to a single object.

Object Identity

Each object has a unique identity which can be checked using id() and compared using is operator.
Equality operator (==) compares values while is compares memory identity.
Syntax: a is b
a = [1,2,3]
b = a
print(a is b)
print(a == b)

The 'is' operator checks memory identity while '==' checks value equality.

Assignment Semantics
In Python, assignment does not create a copy of the object but creates a new reference to the same object. This
behavior leads to shared references where multiple variables point to the same memory location.
Understanding assignment helps avoid unintended changes when working with mutable objects.

Assignment Does Not Copy


Assignment creates a reference to the same object rather than duplicating it. Changes made through
one reference affect the original object if it is mutable.

Syntax: a = b
a = [10,20]
b = a
[Link](30)
print(a)

Appending to list b also changes a because both refer to the same list object.

Rebinding vs Mutation
Rebinding creates a new object while mutation modifies the existing object. Mutable objects can be
changed without rebinding.
Syntax: a = a + [1]
a = [1,2]
b = a
a = a + [3]
print(a)
print(b)

Rebinding creates a new object for a while b still refers to the original list.

Aliasing & Side Effects


Aliasing occurs when multiple variables refer to the same object in memory. This may result in unintended side
effects when one reference modifies the shared object. Aliasing is common in mutable data structures and
functions that modify arguments.

Aliasing in Lists
Aliasing allows two or more variables to point to the same object. Changes through one alias affect all
references.
Syntax: b = a
a = [5,10]
b = a
b[0] = 100
print(a)

Changing b also modifies a due to aliasing between variables.

2.2 Python Parameter Passing Model


Python uses pass-by-object-reference model in function calls. Arguments are passed as references to objects
rather than copied values. Behavior differs based on mutability of the object being passed to the function.

Mutable Arguments
Mutable objects can be modified inside functions affecting the original object. Lists and dictionaries are
examples of mutable objects.
Syntax: func(list)
def modify(lst):
[Link](5)

a=[1,2]
modify(a)
print(a)

The function modifies the original list because the reference to the list is passed.

Immutable Arguments
Immutable objects cannot be changed once created. Reassignment inside functions creates a new
object.
Syntax: func(int)
def change(x):
x = x+5

a=10
change(a)
print(a)

The integer value remains unchanged outside the function.

Mutability and Immutability


Mutability defines whether an object can be modified after creation. Mutable objects allow in-place
modification while immutable objects require rebinding to change values. Choosing correct type helps prevent
unintended side effects.

Immutable Types
Immutable objects such as integers, strings and tuples cannot be modified after creation.
Syntax: a = 5
a=5
a=a+2
print(a)

Rebinding creates a new object instead of modifying the original value.


Mutable Types
Mutable objects such as lists and dictionaries allow in-place modification of elements.
Syntax: [Link]()
a=[1,2]
[Link](3)
print(a)

The list is modified directly without creating a new object.

2.3 Copying Objects


Copying objects helps avoid aliasing issues in programs. Python provides shallow and deep copy mechanisms
for duplicating objects. Shallow copy duplicates references while deep copy creates independent copies.

Shallow Copy
Shallow copy duplicates the top-level object but nested objects remain shared.
Syntax: [Link]()
import copy
a=[1,[2,3]]
b=[Link](a)
b[1][0]=100
print(a)

Nested list modification affects original due to shallow copy.

Deep Copy
Deep copy creates a fully independent copy of nested objects.
Syntax: [Link]()
import copy
a=[1,[2,3]]
b=[Link](a)
b[1][0]=100
print(a)

Original list remains unchanged after deep copy.

2.4 Problem Solving Patterns


Understanding mutability helps in designing efficient and safe programs. Proper handling of mutable objects
prevents logical errors in data processing tasks.

Numeric Processing
Numeric operations should avoid aliasing effects in shared references.
Syntax: sum(list)
nums=[10,20,30]
print(sum(nums))

Numeric aggregation is performed safely using built-in functions.

String Processing
Strings being immutable are safe to manipulate without side effects.
Syntax: [Link]()
text="DATA"
print([Link]())

Lowercase conversion creates a new string object.

Lab Activities
Check identity of two variables using id().
Demonstrate aliasing using list.
Write a function to modify list.
Show shallow vs deep copy behavior.
Explain mutable vs immutable with example.

Lab #03 – Functions, Parameters, Scope & Higher-Order Functions


Functional Abstraction
Functions allow decomposition of complex problems into smaller reusable modules. They improve readability,
maintainability and reduce redundancy in programs. Python supports user-defined functions which
encapsulate logic for reuse across different parts of a program. Functional abstraction helps manage
complexity by separating program logic into independent components.

Function Definition
A function is defined using the def keyword and executed when called. It groups statements to perform
a specific task and returns output when required. Syntax: def func():
def greet(name):
print("Hello", name)

greet("Ali")

The function greet is defined and then called with an argument. It executes the print statement inside the
function body.

Function Parameters
Parameters allow passing values into functions so they can perform operations dynamically. Python supports
positional, default and keyword parameters to increase flexibility of function calls. Proper use of parameters
improves generalization of functions.

Positional Parameters
Arguments are passed in the same order as parameters defined in function. Each argument is assigned
based on its position. Syntax: func(a,b)
def add(a,b):
return a+b

print(add(2,3))

The values 2 and 3 are assigned to parameters a and b based on their position.

Keyword Parameters
Arguments are passed using parameter names allowing flexible ordering of arguments during function
call. Syntax: func(a=,b=)
def add(a,b):
return a+b

print(add(b=2,a=3))

The parameters receive values based on names instead of position.

Default Parameters
Default parameters provide a default value when no argument is passed during function call. Syntax:
def func(x=0):
def power(x=2):
return x*x

print(power())
print(power(4))

If no argument is passed, default value is used otherwise provided value overrides it.

Variable Length Arguments


Python allows passing variable number of arguments using *args and **kwargs. This increases flexibility of
function definitions when number of parameters is unknown.

*args
*args collects multiple positional arguments into a tuple. Syntax: def func(*args):
def total(*nums):
print(sum(nums))

total(2,3,4)

Multiple values are passed and stored in nums tuple.

**kwargs
**kwargs collects multiple keyword arguments into a dictionary. Syntax: def func(**kwargs):
def info(**data):
print(data)

info(name="Ali",age=20)

Keyword arguments are stored as key-value pairs in dictionary.

Scope of Variables
Scope determines the accessibility of variables in different parts of a program. Python follows LEGB rule to
resolve variable names which includes Local, Enclosing, Global and Built-in scopes.

Local Scope
Variables defined inside a function are local and cannot be accessed outside the function. Syntax: def
func():
def demo():
x=5
print(x)

demo()

The variable x is accessible only within the function.

Global Scope
Variables defined outside functions are global and accessible throughout the program. Syntax: global x
x=10
def show():
print(x)

show()

The global variable x is accessed inside the function.

nonlocal Keyword
nonlocal allows modifying variable in enclosing scope inside nested functions. Syntax: nonlocal x
def outer():
x=5
def inner():
nonlocal x
x=10
inner()
print(x)

outer()

The inner function modifies variable from enclosing scope.

Higher Order Functions


Higher Order Functions are functions that take other functions as arguments or return functions as output.
They support functional programming techniques.

Lambda Functions
Lambda functions are anonymous one-line functions used for short operations. Syntax: lambda x:
square=lambda x:x*x
print(square(4))

The lambda expression returns the square of given number.

map Function
Map applies a function to all elements of a sequence. Syntax: map(func,list)
nums=[1,2,3]
res=list(map(lambda x:x*2,nums))
print(res)

Each element is doubled using map with lambda.


filter Function
Filter selects elements from sequence based on condition. Syntax: filter(func,list)
nums=[1,2,3,4]
res=list(filter(lambda x:x%2==0,nums))
print(res)

Only even values are selected.

Lab Activities
Write a function using default parameters.
Demonstrate use of *args.
Show global and nonlocal keyword example.
Use lambda to square numbers.
Use filter to extract even numbers.

Lab #04 – Errors, Exceptions & Defensive Programming


Understanding Errors
Errors occur when a program fails to execute correctly due to incorrect syntax, invalid operations or wrong
logic. In Python, errors interrupt the normal flow of program execution. Understanding types of errors helps
developers debug programs efficiently and write reliable software. Proper error handling improves stability
and prevents unexpected program crashes.

Syntax Errors
Syntax errors occur when program violates Python grammar rules during compilation. These errors are
detected before execution and must be corrected before running the program. Syntax: print('Hello'
print('Hello'

The interpreter raises a syntax error due to missing closing bracket.

Runtime Errors
Runtime errors occur during program execution due to invalid operations such as division by zero or
invalid type conversion. Syntax: x/0
x=5
print(x/0)

Division by zero generates ZeroDivisionError at runtime.

Exception Handling
Exceptions are runtime errors that can be handled using try-except blocks. Handling exceptions ensures
smooth execution of programs without abrupt termination. Python provides structured mechanisms to detect
and recover from errors during execution.

try-except Block
try block tests a block of code for errors while except handles the exception when it occurs. Syntax: try:
try:
x=5/0
except ZeroDivisionError:
print("Error occurred")

The exception is caught and handled without program crash.

else Block
else block executes when no exception occurs in try block. Syntax: else:
try:
x=5/1
except:
print("Error")
else:
print("No Error")

The else block runs when no exception occurs.

finally Block
finally block executes regardless of exception occurrence. Syntax: finally:
try:
x=5/0
except:
print("Error")
finally:
print("Done")

The finally block always executes.

raise Statement
The raise statement is used to generate custom exceptions manually when invalid conditions are detected. It
helps implement input validation and enforce constraints in programs.

User Defined Exception


raise allows developer to trigger exception when condition is violated. Syntax: raise ValueError()
age=15
if age<18:
raise ValueError("Age must be 18+")

The program manually raises exception when validation fails.

Defensive Programming
Defensive programming ensures that programs handle unexpected input safely. It includes input validation,
assertions and exception handling to prevent logical failures during execution.

Assertions
Assertions test assumptions in program logic and raise error when condition becomes false. Syntax:
assert condition
x=5
assert x>0
print("Valid")

If condition fails, AssertionError occurs.

File I/O Exception Handling


Exception handling can also be applied while working with files to avoid program crash when file is not found
or cannot be opened.

File Handling Exception


File operations may generate runtime exceptions such as FileNotFoundError. Syntax: open(file)
try:
f=open("[Link]")
except FileNotFoundError:
print("File not found")

The exception is handled safely without program termination.

Lab Activities
Write program to handle division by zero.
Use try-except for input validation.
Demonstrate finally block.
Use raise to validate age input.
Handle file not found exception.

Lab #05 – Introduction to OOP, Members & Methods


1. Object Oriented Programming

OOP is a programming paradigm that models real-world entities as objects. It improves modularity, reusability and
maintainability by grouping data and behavior together. Python supports OOP using classes and objects to represent
real systems such as sensors, students or bank accounts.1
1. Class as Blueprint

A class is a user-defined blueprint used to create objects. It defines the properties (attributes) and
behaviors (methods) of objects.
Syntax: class ClassName:

class Student:
pass
The class Student defines a blueprint which can be used to create multiple student objects.
2. Object Creation
An object is an instance of a class that contains actual data and behavior defined by the class. Objects are created to
represent real-world entities and interact with each other using methods.
Instantiation

Instantiation is the process of creating an object from a class using its constructor.
Syntax: obj = ClassName()

class Student:
pass

s1 = Student()
print(type(s1))
An object s1 is created from Student class and its type confirms successful instantiation.
3. Constructor Method

Constructors are special methods used to initialize object attributes during creation. Python provides __init__
method which is automatically invoked when an object is instantiated.
__init__ Method

The __init__ method initializes attributes of an object when it is created.


Syntax: def __init__(self):

class Student:
def __init__(self,name):
[Link] = name

s1 = Student("Ali")
print([Link])
The constructor assigns the name attribute to the object during creation.
4. Instance Attributes

Instance attributes are variables that belong to individual objects. Each object maintains its own copy of instance
attributes which can store different values.
2. Attribute Assignment

Attributes are assigned using self keyword inside the class.


Syntax: [Link] = value

class Student:
def __init__(self,age):
[Link] = age

s1 = Student(20)
print([Link])
The attribute age is stored separately for each object.
5. Instance Methods

Instance methods define behavior of objects and operate on instance attributes. These methods must include self as
first parameter.

Method Definition
Methods are functions defined inside class and are invoked using object.
Syntax: def method(self):

class Student:
def show(self):
print("Student Info")

s1 = Student()
[Link]()
The method show is invoked using the object s1.
6. Class vs Instance Attributes

Class attributes are shared among all objects while instance attributes are unique to each object. This helps manage
common and individual data.
Class Attribute

Class attributes are defined inside class but outside methods.


Syntax: class_var = value

class Student:
school = "ABC"

s1 = Student()
print([Link])
The attribute school is shared by all objects of Student class.
Object Introspection

Object introspection allows checking attributes and methods of an object during runtime. It helps debugging and
understanding object structure.
dir() Function

The dir() function returns all attributes and methods of an object.


Syntax: dir(obj)

class Student:
pass

s1 = Student()
print(dir(s1))
The dir() function lists attributes and methods of object.
7. Lab Activities

• Create class for BankAccount with attributes.


• Create student class that takes name & marks of 3 subjects as arguments in
constructor. Then create a method to print average.
• Create method to display details through object introspection.
• [Link] class attribute.
Lab #06 – Encapsulation, Attributes & Properties

Encapsulation in Object-Oriented Programming


Encapsulation is one of the core principles of object-oriented programming. It means combining data
(attributes) and behavior (methods) inside a single class while restricting direct access to internal data. This
protects objects from invalid modifications and ensures that data is accessed in a controlled way. In
engineering and scientific systems this is important because incorrect values such as negative pressure or
temperature may produce unrealistic results. Encapsulation helps maintain safe and meaningful object
states.
Syntax
class ClassName:
def __init__(self, value):
self._attribute = value //store object internal data, underscore indicates it is for internal use.

Example
class Reactor:

def __init__(self, temperature):

self._temperature = temperature # stores the temperature, Underscore means variable is not directly
accessed outside the class(protected attribute)

def show_temperature(self): #A method to display temperature.


print("Temperature:", self._temperature)

r = Reactor(25) # create reactor object with temperature 25

r.show_temperature() #call function to display temperature.

Output: Temperature: 25
Explanation
_temperature is a protected attribute. The method show_temperature() provides controlled access and
avoids direct misuse of internal data.

2. Attribute Visibility Levels

Python supports three levels of attribute visibility used for controlling access to class data.

Public Attributes
Public attributes are accessible from anywhere in the program and can be read or modified directly.
Example
class Student:
def __init__(self, name):
[Link] = name

s = Student("Ali")
print([Link]) # It can be accessed directly using [Link].

Explanation: name is a public attribute. To access a public attribute in Python, we


use dot notation:[Link]

Protected Attributes
Protected attributes are indicated by a single underscore (_) prefix. They are intended for internal use within
class and its derived (child) classes.
Example
class Engine:
def __init__(self, power):

self._power = power
# _power is an attribute of the class. Single _ (underscore) indicates it is protected. It store values pass to
the constructor e.g: _power = 200

e = Engine(200)
# An object e of class Engine is created. The constructor runs automatically.
The value 200 is assigned to _power.

print(e._power)
# Python accesses attributes using dot notation: object_name.attribute_name

Key idea: Protected attributes help protect internal data while still allowing subclasses to use it,
improving code safety and structure.

Private Attributes
Private attributes are variables that can only be accessed inside the same class.
Working:
Private attributes use double underscores __ before the variable name.
Example
class Account:

def __init__(self, balance):


self.__balance = balance
# __balance is a private attribute because it starts with double underscores __.Private attributes are meant
to be used only inside the class. Value stored: __balance = 1000

a = Account(1000)
# A new object a is created. The constructor [Link] value 1000 is assigned to __balance.

Name Mangling (Important Logic)


Python does not store the private attribute exactly as __balance.
Python automatically changes the name using name mangling(Private attributes are meant to be used only
inside the class. Name mangling makes it harder to access them directly from outside.):
__balance → _Account__balance
Rule:
__variable → _ClassName__variable
So internally the object stores:
_Account__balance = 1000

print(a._Account__balance)
# a → object name
. → access operator
_Account__balance → mangled attribute name

3. Getter Methods
Getter methods are used to retrieve the value of a private or protected attribute. Instead of accessing the
variable directly like object._attribute. the program uses a method to read the value safely. Getter methods
improve data security and provide controlled access to internal variables.
Syntax
def get_attribute(self):
return self._attribute
Example
class Reactor:
def __init__(self):
self._temperature = 25 # The underscore _ indicates: this attribute should not be accessed directly. it is
protected

def get_temperature(self): # This method retrieves the temperature.


return self._temperature # method returns the value that was stored inside the object to the place
where the method was called.

r = Reactor() # Creates an object named r.


print(r.get_temperature()) # Calls the getter method.
Output: 25

4. Setter Methods
Setter methods modify attribute values inside a class. They prevent invalid values from being assigned to
attributes and ensure objects remain in valid states. Instead of changing the variable directly like this:
object._attribute = value we use a method to update the value.
The setter method can check if the new value is valid before storing it.
Key Idea: A Setter method is a function that updates an attribute while checking whether the value is
correct or not.
Why Setter Methods Are Used
Setter methods are important because they protect the object's data.
[Link] Invalid Values
Sometimes values must follow rules.
Examples:
• Temperature cannot be negative
• Bank balance cannot be less than zero
• Age cannot be negative
Setter methods check these conditions.

Syntax
def set_attribute(self, value):
self._attribute = value

Example
class Reactor:
def __init__(self): #(constructor) When the object is created, the temperature is set to 25.
self._temperature = 25 #Protected Variable

def set_temperature(self, value): # This method allows us to change the temperature.

if value < 0: # The program checks if the temperature is [Link] temperature is


considered invalid in this example.

print("Invalid temperature" If the value is invalid, the program does not store it.)

else:
self._temperature = value # If the value is valid, it updates the temperature.

r = Reactor()
r.set_temperature(50) # Temperature becomes 50.

Expected Output
If we try:
r.set_temperature(-10)
Output:
Invalid temperature
The temperature does not change.

5. Properties in Python
Properties allow methods to be accessed like attributes while still performing validation internally. They
provide cleaner syntax compared to traditional getter and setter methods.
Example:
Normally when we use a method we write:
object.get_temperature()
But with properties, we can access the method like a variable:
[Link]
So a property makes code cleaner and easier to read.

Syntax
@property #This is class decorator It tells Python that this method should behave like an attribute.
def attribute(self): #This defines the property [Link] name of the method becomes the attribute
name.
return self._attribute #The method returns the stored value.

Example
class Reactor:
def __init__(self):
self._temperature = 25

@property
def temperature(self): #This creates a property called temperature.
return self._temperature #method returns the stored temperature.

r = Reactor()
print([Link]) #Even though temperature is a method, we use it like an attribute.

Output: 25
Getter vs Property

Method Type Syntax

Getter Method r.get_temperature()

Property [Link]

Both return the same value, but property syntax is cleaner.

6. Property Setter
Property setters allow assigning values while performing validation(checking if the value is valid).
It works together with @property.

@property → used to read the value


@[Link] → used to update the value
A property setter is a method that updates a property value and can check if the value is correct before
storing it.

Syntax
@[Link] #This decorator connects the setter to the property. attribute must be the same name as
the property method.
def attribute(self, value): #Defines the setter function. value is the new value we want to assign.
self._attribute = value #Stores the new value inside the object.
Example
class Reactor:
def __init__(self):
self._temperature = 25

@property
def temperature(self): #This allows us to read the temperature like a variable.
return self._temperature

@[Link] #This allows us to assign a new value to the property.


def temperature(self, value):

if value < 0: #The program checks if the temperature is negative.

raise ValueError("Temperature cannot be negative") #Error handling: This stops the program and
shows an error message.
self._temperature = value #The new temperature is stored.
r = Reactor()
print([Link])
Output: 25

7. Computed Properties
A Computed Property is a property whose value is calculated using other attributes instead of being stored
directly. This means the value is computed every time we access it.
Simple Definition
A computed property is a property that calculates its value from other variables instead of storing the value
in memory.
Why Computed Properties Are Used
Computed properties are useful when a value depends on other variables.
Instead of storing the value permanently, the program calculates it whenever needed.
Advantages
1. No redundant data storage
2. Value always stays updated
3. Less chance of incorrect data
4. Cleaner program design

Example
class GasSample:
R = 0.082 #R is the gas constant. It is a class attribute, meaning all objects share the same value.

def __init__(self, n, T, V): #The constructor receives three values: (n → number of moles T →
temperature V → volume) These are stored in the object.

self.n = n
self.T = T #When an object of the class is created, the constructor receives values
self.V = V

@property
def pressure(self): #This defines a property called pressure. But pressure is not stored anywhere.
Instead, it is calculated when accessed.

return (self.n * GasSample.R * self.T) / self.V #This calculates pressure using the formula: P = nRT / V
Example Usage
gas = GasSample(2, 300, 10)

print([Link])
Output (approximately):
4.92
The program calculates pressure when we ask for it.

8. Read-Only Properties
A property becomes read-only when only a getter exists and no setter is defined. Such properties allow
reading values but prevent modification.
It happens when we define @property and do not define a setter.
Example
class Experiment:
def __init__(self, id): #When an object is created, the constructor receives the experiment ID.
self._id = id #(Protected Variable) The ID is stored inside the object.

@property
def id(self): #This creates a property called id.
return self._id This allows the program to read the ID.

Using the Property


exp = Experiment(101)

print([Link])
Output
101
Here we read the value using the property.

2. Why Read-Only Properties Are Used


Sometimes certain values should never be modified after the object is created.
Examples:
• Experiment ID
• Student Roll Number
• Product Serial Number
• Account Number
These values should remain fixed.
Read-only properties help protect such data.
9. Unit Conversion Properties
Properties can also perform unit conversions dynamically. A class may store data in one unit but allow
access in another unit.
Example
• Store temperature in Celsius
• Access it in Fahrenheit
The conversion happens automatically inside the class.

class Temperature:

def __init__(self, celsius):


self._c = Celsius #When we create an object, the temperature is stored in Celsius.

@property
def C(self):
return self._c #This property returns the temperature in Celsius.

@property
def F(self):
return (self._c * 9/5) + 32 #This property converts Celsius to Fahrenheit.

@[Link]
def F(self, value):
self._c = (value - 32) * 5/9 #This property converts Celsius to Fahrenheit.
Example Usage
t = Temperature(25)

print(t.C) # Celsius
print(t.F) # Fahrenheit

t.F = 86

print(t.C)
Output
25
77
30
Practice Tasks for Students
1. Create a BankAccount class with private balance and getter/setter validation.
2. Create a class PressureVessel that stores pressure and ensures pressure remains between 0 and 500 bar.
3. Create a Rectangle class with length and width and property area.
4. Create a Temperature converter supporting Celsius and Fahrenheit.
5. Create a Student class where grade property only accepts values between 0 and 100.

Lab #07 – Magic Methods & Operator Overloading


1. Introduction to Magic Methods
Magic methods (also known as dunder methods) are special methods in Python that begin and end with double
underscores. These methods allow objects of a class to interact with built-in operators and functions such as print(),
len(), +, or <. Python automatically invokes these methods when specific operations occur. Using magic methods
allows developers to create intuitive object-oriented programs where custom objects behave like built-in data types.
1.1 Understanding Magic Methods
Magic methods extend the behaviour of objects and are automatically triggered by Python when an operation
occurs on the object. Syntax: def __method__(self):
class Example:
def __init__(self,value):
[Link]=value

obj=Example(10)
print([Link])
The __init__ magic method is executed automatically when an object is created and initializes the object's
attributes.
2. Object Representation Methods
Representation methods define how objects appear when printed or inspected. They help display meaningful
information about objects and are useful for debugging and user-friendly output. Two commonly used
representation methods are __str__ and __repr__.
2.1 __str__ Method
The __str__ method returns a human-readable string representation of an object and is used when the object is
printed. Syntax: def __str__(self):
class Material:
def __init__(self,name,density):
[Link]=name
[Link]=density

def __str__(self):
return f"{[Link]} with density {[Link]} g/cm3"

m=Material("Steel",7.8)
print(m)
The print() function automatically calls the __str__ method and displays a readable description of the object.
2.2 __repr__ Method
The __repr__ method returns a precise representation of the object used mainly for debugging. Syntax: def
__repr__(self):
class Material:
def __init__(self,name,density):
[Link]=name
[Link]=density

def __repr__(self):
return f"Material(name='{[Link]}',density={[Link]})"

m=Material("Steel",7.8)
print(repr(m))
The repr() function calls the __repr__ method and shows detailed information about the object.
3. Arithmetic Operator Overloading
Operator overloading allows custom objects to work with arithmetic operators such as +, -, *, and /. This feature is
useful for mathematical modeling such as vectors, physical quantities, or engineering values. Magic methods define
how objects behave when these operators are used.
3.1 __add__ Method
The __add__ method defines the behaviour of the addition operator between objects. Syntax: def
__add__(self,other):
class GasSample:
def __init__(self,moles):
[Link]=moles

def __add__(self,other):
return GasSample([Link]+[Link])

g1=GasSample(4)
g2=GasSample(6)

total=g1+g2
print([Link])
The + operator triggers the __add__ method and creates a new GasSample object containing combined moles.
3.2 __mul__ Method
The __mul__ method defines behaviour of the multiplication operator. Syntax: def __mul__(self,value):
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)

v=Vector(2,3)
v2=v*2

print(v2.x,v2.y)
The vector components are multiplied by the scalar value and a new vector object is returned.
4. Comparison Magic Methods
Comparison magic methods allow objects to be compared using relational operators such as <, >, or ==. These
methods are useful when sorting objects or performing logical comparisons.
3. 4.1 __lt__ Method
The __lt__ method defines the behaviour of the less-than operator. Syntax: def __lt__(self,other):
class Material:
def __init__(self,name,density):
[Link]=name
[Link]=density

def __lt__(self,other):
return [Link]<[Link]

steel=Material("Steel",7.8)
al=Material("Aluminum",2.7)

print(al<steel)
The comparison checks density values and returns True if aluminum is less dense than steel.
5. Container Protocol
The container protocol allows custom classes to behave like Python lists. By implementing methods such as __len__
and __getitem__, objects can support indexing and length operations.
5.1 __len__ Method
The __len__ method defines behaviour of the len() function for objects. Syntax: def __len__(self):
class TempLog:
def __init__(self):
[Link]=[20,21,22]

def __len__(self):
return len([Link])

log=TempLog()
print(len(log))
4. The len() function calls __len__ and returns the number of elements stored in the container.
5.2 __getitem__ Method
The __getitem__ method allows accessing elements using index notation. Syntax: def __getitem__(self,index):
class TempLog:
def __init__(self):
[Link]=[20,21,22]

def __getitem__(self,index):
return [Link][index]

log=TempLog()
print(log[1])
5. The object behaves like a list and allows element access using brackets.
6. 6. Iteration Protocol
7. The iteration protocol allows objects to work with loops such as for loops. Implementing __iter__ and
__next__ makes objects iterable similar to built-in collections.
6.1 Iterator Example
The __iter__ method returns the iterator object while __next__ returns the next value in the sequence. Syntax: def
__iter__(self):
class BatchReactor:
def __init__(self,batches):
[Link]=batches
[Link]=0

def __iter__(self):
return self

def __next__(self):
if [Link]>=len([Link]):
raise StopIteration
batch=[Link][[Link]]
[Link]+=1
return batch

r=BatchReactor(["B1","B2","B3"])

for b in r:
print(b)
The loop repeatedly calls __next__ until StopIteration occurs indicating the end of iteration.
7. Callable Objects
Objects can behave like functions when the __call__ method is implemented. This allows objects to maintain
internal state while still being invoked like normal functions.
7.1 __call__ Method
The __call__ method allows objects to be used like functions. Syntax: def __call__(self,x):
class ReLU:
def __call__(self,x):
return max(0,x)

f=ReLU()

print(f(-5))
print(f(4))
The object behaves like a function and applies the ReLU activation logic.
8. Context Managers
Context managers manage resources automatically using the with statement. The __enter__ method runs when
entering the block and __exit__ runs when leaving the block.
8.1 __enter__ and __exit__ Methods
These methods control initialization and cleanup when using with statements. Syntax: def __enter__(self):
class Logger:
def __enter__(self):
print("Start logging")
return self

def __exit__(self,exc_type,exc,value):
print("End logging")

with Logger():
print("Inside block")
The program prints messages when entering and leaving the context block.
9. Lab Activities
Create a Material class implementing both __str__ and __repr__ methods.
Create a GasSample class that supports addition using + operator.
Create a Vector class that supports multiplication with scalar.
Create a container class implementing __len__ and __getitem__.
Implement an iterator class that prints batch IDs sequentially.
Create a callable object similar to ReLU activation.
Create a Logger context manager using with statement.

You might also like