adv python
adv python
Data Structures: Problem solving using Python Data Structures: LIST, DICT, TUPLES and SET-
Functions and Exceptions – Lamda Functions and Parallel processing – MAPS – Filtering - Itertools – Generators.
1. LIST
A list is an ordered, mutable collection of elements that allows duplicates. It is one of the most commonly used data
structures in Python.
Characteristics:
· Ordered: Elements maintain their insertion order.
· Mutable: Items can be modified (added, removed, or changed).
· Allows Duplicates: The same value can appear multiple times.
· Indexing & Slicing Supported: Elements can be accessed via indexes.
· Can Contain Different Data Types: A list can store integers, strings, other lists, etc.
Operations on List:
Operation Description Example Output
Creation Define a list lst = [1, 2, 3, 4] [1, 2, 3, 4]
Accessing Elements Retrieve elements using an index lst[0] 1
Slicing Get a subset of the list lst[1:3] [2, 3]
Append Add an element at the end lst.append(5) [1, 2, 3, 4, 5]
Insert Insert at a specific index lst.insert(2, 10) [1, 2, 10, 3, 4]
Remove Remove first occurrence of a value lst.remove(2) [1, 10, 3, 4]
Pop Remove element at index (default last) lst.pop(1) [1, 3, 4]
Reverse Reverse list order lst.reverse() [4, 3, 1]
Sort Sort the list lst.sort() [1, 3, 4]
Length Get the number of elements len(lst) 3
2. Dictionary
A dictionary is an unordered, mutable collection of key-value pairs, where each key is unique.
· Unordered (before Python 3.7), but maintains insertion order from Python 3.7+.
· Mutable: Keys and values can be modified.
· Keys Must Be Unique: No duplicate keys are allowed.
· Keys Must Be Immutable: Strings, numbers, and tuples can be keys.
· Efficient Lookup: Provides fast key-based access.
3. Tuple
A tuple is an ordered, immutable collection of elements that allows duplicates.
· Ordered: Elements maintain their insertion order.
· Immutable: Once created, elements cannot be modified.
· Allows Duplicates: The same value can appear multiple times.
· Faster than Lists: Tuples provide better performance for read-only operations.
Operation Description Example Output
Creation Define a tuple tpl = (1, 2, 3, 4) (1, 2, 3, 4)
Accessing Elements Retrieve elements using an index tpl[0] 1
Slicing Get a subset of the tuple tpl[1:3] (2, 3)
Length Get the number of elements len(tpl) 4
Count Count occurrences of a value tpl.count(2) 1
Index Find the first occurrence of a value tpl.index(3) 2
Concatenation Merge two tuples tpl + (5, 6) (1, 2, 3, 4, 5, 6)
Unpacking Assign values to variables a, b, c, d = tpl a=1, b=2, c=3, d=4
4. Set
A set is an unordered collection of unique elements.
· Unordered: Elements do not follow a specific sequence.
· Mutable: Can add or remove elements, but individual elements cannot be changed.
· No Duplicates: Automatically removes duplicate values.
· Supports Mathematical Operations: Intersection, union, difference, etc.
FUNCTIONS
A function is a self-contained block of code that performs a specific task.
Functions promote code reuse, modularity, and readability.
Python has:
o Built-in functions like print(), len(), max()
o User-defined functions which you can create with def
o Anonymous functions (lambda functions)
Syntax of a Function
def function_name(parameters):
"""docstring (optional)"""
# body
return value
Key Concepts:
Concept Description
def Keyword to define a function
Parameters Input values (optional)
return Output value (optional)
Docstring Describes what the function does
1. Defining a Function
def greet(name):
return f"Hello, {name}!"
#You can now call this function:
print(greet("Alice")) # Output: Hello, Alice!
def greet(name="Friend"):
return f"Hello, {name}!"
print(greet()) # Hello, Friend
5. Variable-Length Arguments
def total_sum(*args): # any number of positional arguments
return sum(args)
Common Exceptions:
Exception Description Code Example
SyntaxError Code has incorrect syntax. # print("Hello"
NameError Using a variable that hasn't been defined. print(x) when x is not defined
TypeError Operation or function applied to the wrong type. '2' + 2
IndexError Accessing an index that doesn’t exist. [1, 2][5]
KeyError Accessing a dictionary key that doesn't exist. {"a": 1}["b"]
ValueError Correct type, but inappropriate value. int("abc")
ZeroDivisionError Dividing a number by zero. 10 / 0
FileNotFoundErro Trying to open a file that doesn't exist. open("no.txt")
r
AttributeError Invalid attribute access for an object. 10.append(5)
ImportError Import fails because module doesn't exist. import fake_module
IndentationError Incorrect indentation in the code. def f():\nprint("Hi")
RuntimeError Errors that don't fall into other categories. Recursive call with no base
case
Handling Exceptions
Exception handling is a mechanism in Python that allows you to deal with runtime errors (exceptions) in a controlled
manner. When an error occurs, it raises an exception which interrupts the normal flow of the program. By using exception
handling, you can catch and handle these exceptions, allowing the program to continue running smoothly or give
meaningful feedback to the user.
Syntax :
try: # This is where you write code that may raise an exception
except SomeException as e: # This block handles the exception
else: # This block runs if no exception occurs
finally: # This block will always run, regardless of whether an exception occurred
Example:
try:
num1 = int(input("Enter the numerator: "))
num2 = int(input("Enter the denominator: "))
result = num1 / num2
except ZeroDivisionError:
print("You cannot divide by zero.")
except ValueError:
print("Please enter valid integers.")
else:
print("The result is:", result)
finally:
print("Program execution completed.")
Sequential vs Parallel:
Sequential: Tasks are performed one after the other
Parallel: Tasks are split and run simultaneously on different processors/cores
Python Module Used: multiprocessing — built-in module that allows the creation of separate processes
def print_numbers():
for i in range(5):
print(i)
time.sleep(1)
if __name__ == "__main__":
p1 = multiprocessing.Process(target=print_numbers)
p2 = multiprocessing.Process(target=print_numbers)
p1.start()
p2.start()
p1.join()
p2.join()
This runs print_numbers() in two separate processes at the same time.
map() FUNCTION
map() is used to apply a function to every item in an iterable (like a list or tuple). It returns a map object, which is an
iterator. Ideal for transforming data without using loops.
filter() FUNCTION
filter() is used to filter out elements from an iterable based on a condition (True/False). It removes items that do not
meet a condition.
SYNTAX: filter(function, iterable)
a = [1, 2, 3, 4, 5, 6]
b = filter(even, a)
GENERATORS
A generator is a special function that returns an iterator and generates items one at a time using the yield keyword.
Memory-efficient and ideal for large data sequences or infinite loops.
SYNTAX:
def my_generator():
yield value
EXAMPLE:
def countdown(n):
while n > 0:
yield n
n -= 1
Classes & Objects: Classes as User Defined Data Type, Objects as Instances of Classes, Creating Class and
Objects, Creating Objects by Passing Values, Variables & Methods in a Class Data, Abstraction, Data Hiding,
Encapsulation, Modularity, Inheritance, Polymorphism
CLASS
A class in Python is a blueprint for creating objects. Objects are instances that contain data (attributes) and functions
(methods) to operate on that data. A User Defined Data Type is a data structure created by the programmer to represent
real-world entities.
def method1(self):
# Some behavior
pass
class keyword defines a class.
__init__() is the constructor. It runs when you create an object.
self refers to the current object.
You define attributes and methods inside it.
def display(self):
print("Name:", self.name)
print("Roll No:", self.roll)
print("Marks:", self.marks)
Here, Student is a user-defined data type with:
Data (name, roll, marks)
Behavior (display())
s1.display()
s2.display()
OUTPUT:
Name: Alice
Roll No: 101
Marks: 95
Name: Bob
Roll No: 102
Marks: 87
key concepts
Term Description
Class Blueprint for creating objects (user-defined type)
Object Instance of a class (holds data + functions)
Attribute Variables in class (e.g., name, roll)
Method Functions defined inside a class
Constructor Special method that runs when the object is created
(__init__)
Self Reference to the current object
def display(self):
print(f"Name: {self.name}, Roll: {self.roll}")
Object Attributes
You can also access or modify attributes directly:
print(s1.name) # Alice
s1.name = "Alicia"
print(s1.name) # Alicia
CREATING A CLASS
A class is a blueprint for creating objects. It defines attributes (variables) and methods (functions).
Syntax:
class ClassName:
def __init__(self): # Constructor
# attributes
def method_name(self):
# behavior
__init__() is a constructor. It runs automatically when the object is created.
self refers to the current object (like this in other languages).
CREATING OBJECTS
An object is an instance of a class.
Syntax: obj = ClassName()
Example:
p1 = Person()
p2 = Person()
Output:
A new person is created!
A new person is created!
Each time you call the class, it creates a new object and runs the constructor.
def display(self):
print(f"Name: {self.name}, Age: {self.age}")
VARIABLE
Variables are used to store data related to the class or its instances. Variables inside a class can be of two types:
s1 = Student("Alice")
s2 = Student("Bob")
Types of Methods:
Type Defined with Access
Instance Method self Can access both instance & class variables
Class Method @classmethod + cls Can access only class variables
Static Method @staticmethod Cannot access self or cls
1. Instance Method
Definition: Operates on instance variables of the object.
First parameter: Always self.
Usage: Most common type; used to access or modify object-specific data.
class Student:
def __init__(self, name):
self.name = name
2. Class Method
Definition: Operates on the class variables; affects all instances.
First parameter: Always cls. Decorator: @classmethod
class Student:
school = "ABC School"
@classmethod
def change_school(cls, new_name): # Class method
cls.school = new_name
3. Static Method
Definition: Does not access instance or class data; behaves like a regular function inside a class.
No self or cls parameter Decorator: @staticmethod
class MathTools:
@staticmethod
def add(a, b): # Static method
return a + b
Real-Life Analogy
Imagine a "Student" class:
Each student has a name and age → instance variables.
All students go to "XYZ School" → class variable.
Each student can introduce themselves → instance method.
The school name can be updated for everyone → class method.
A static method can be used to check if a student is eligible (like age >= 18) → static method.
ABSTRACTION
Abstraction is the process of hiding internal details and showing only essential information to the user.
Focuses on what an object does, rather than how it does it.
Achieved using abstract classes and interfaces (in Python: via abc module).
# Abstract Class
class Animal(ABC):
@abstractmethod
def sound(self):
pass
class Cat(Animal):
def sound(self):
return "Meow"
c = Cat()
print(c.sound()) # Output: Meow
DATA HIDING
Data Hiding is an OOP concept used to restrict direct access to some of an object's components (like variables), for
security and data integrity.
It prevents the accidental modification of data and ensures controlled access.
Real-Life Analogy:
Think of a bank account:
You can see your balance using an app (interface).
But you can’t directly access or modify the account data in the bank’s system — it’s protected.
Any update has to go through proper methods (like deposit or withdraw).
That’s data hiding in action!
e = Employee("John", 50000)
e.display_info()
Private Variable: self.__salary is a private variable. It can't be accessed directly from outside the class.
Public Method: The display_info() method is a public method and can be called from outside the class to access
information.
Private Method: __private_method() is a private method. It can only be accessed within the class, not from outside.
Access Control: If you try to directly access the private variable __salary outside the class (e.g., e.__salary), Python will
raise an AttributeError.
ENCAPSULATION
Encapsulation is the concept of wrapping data (variables) and methods (functions) that operate on the data within a
single unit — the class. It helps to restrict direct access to some of the object’s components and provides controlled
access via methods.
Real-Life Analogy:
Think of a capsule (medicine):
It contains a mixture of drugs (data + operations) inside a shell.
You don’t see the individual components — you just take the capsule.
Similarly, in programming, a class encapsulates data and functions.
Encapsulation in Python
Use classes to encapsulate data and functions.
Use access modifiers:
o public: accessible from anywhere
o protected (_var): suggest restricted use
o private (__var): name-mangled, not directly accessible
Example
class Person:
def __init__(self, name, age):
self.__name = name # Private attribute
self.__age = age # Private attribute
Explanation:
1. Private Attributes: __name and __age are private attributes of the Person class.
2. Public Method: get_details() is a public method used to access the private attributes safely.
3. Encapsulation: The class encapsulates the data (name and age) and allows controlled access via the get_details()
method, protecting the internal data from direct modification
MODULARITY
Modularity is the concept of breaking a large program into smaller, manageable, reusable, and independent
components called modules.
Each module contains everything necessary to execute one aspect of the desired functionality.
In Python, modules are simply .py files that contain classes, functions, or variables.
You can import and use them in other files.
Example:
# Module: operations.py
def multiply(a, b): return a * b
# Main code
import operations
result = operations.multiply(3, 4)
print("Product:", result)
Explanation:
1.Modular Code: The multiplication function is defined in a separate module (operations.py), keeping the code organized.
2. Main Program: The main code imports the operations module and uses its multiply() function to compute the result.
3. Reusability: The operations module can be reused across different programs, demonstrating modularity.
Modularity vs Other OOP Concepts
Concept What It Does
Encapsulation Bundles data + methods in a class
Abstraction Hides complexity; shows only essentials
Modularity Breaks system into manageable pieces
Inheritance Enables reusability from parent to child
Polymorphism Same method behaves differently in
classes
INHERITANCE
Inheritance is an Object-Oriented Programming (OOP) concept where a child class (subclass) inherits attributes and
methods from a parent class (superclass).
This allows code reuse and makes it easier to maintain and scale programs.
Real-Life Analogy:
Imagine a parent has certain characteristics, like a surname, and habits.
Their child inherits many of these traits — but can also have some of their own! In programming, the child class gets
access to the parent class properties and behaviors, but can also add or override them.
Basic Example:
# Parent Class
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name} makes a sound.")
# Child Class
class Dog(Animal):
def speak(self):
print(f"{self.name} barks!")
d = Dog("Tommy")
d.speak()
Explanation:
Dog inherits from Animal
It gets __init__() from the parent class
But it overrides the speak() method
POLYMORPHISM
Polymorphism is an OOP concept that allows objects of different classes to be treated as objects of a common
superclass. It also allows the same method to behave differently depending on the object calling it.
In simpler terms:
One interface, multiple behaviors.
It allows the same method name to behave differently based on the object type.
Real-Life Analogy:
Think of a shape:
You have a generic shape that can have a draw() method.
But you also have circle, rectangle, and triangle subclasses.
Each subclass has its own way of drawing.
class Shape:
def draw(self):
print("Drawing a generic shape")
class Circle(Shape):
def draw(self):
print("Drawing a Circle")
class Rectangle(Shape):
def draw(self):
print("Drawing a Rectangle")
g = Greet()
g.say_hello() # Calls with no arguments
g.say_hello("Alice") # Calls with one argument
Explanation: This is a simple way of simulating method overloading by using default arguments.
Unit 2
Python Mulithreading : Python Multithtreading and Multiprocessing ant their basics- Threading module and
example, multithreaded priority queue
Python Multithtreading
A thread is an entity within a process that can be scheduled for execution. Also, it is the smallest
unit of processing that can be performed in an OS (Operating System). In simple words, a thread is
a sequence of such instructions within a program that can be executed independently of other
code. For simplicity, you can assume that a thread is simply a subset of a process! A thread
contains all this information in a Thread Control Block (TCB) :
Multithreading is defined as the ability of a processor to execute multiple threads concurrently. In a simple,
single-core CPU, it is achieved using frequent switching between threads. This is termed context switching .
In context switching, the state of a thread is saved and the state of another thread is loaded whenever any
interrupt (due to I/O or manually set) takes place. Context switching takes place so frequently that all the
threads appear to be running parallelly (this is termed multitasking ). In Python , the threading module
provides a very simple and intuitive API for spawning multiple threads in a program.
Steps for creating thread in multithreading:-
Step 1: Import Module
First, import the threading module.
import threading
Step 2: Create a Thread
To create a new thread, we create an object of the Thread class. It takes the 'target' and 'args' as the
parameters. The target is the function to be executed by the thread whereas the args is the arguments to be
passed to the target function.
t1 = threading.Thread(target, args)
t2 = threading.Thread(target, args)
Step 3: Start a Thread
To start a thread, we use the start() method of the Thread class.
t1.start()
t2.start()
Step 4: End the thread Execution
Once the threads start, the current program (you can think of it like a main thread) also keeps on executing. In
order to stop the execution of the current program until a thread is complete, we use the join() method.
t1.join()
t2.join()
As a result, the current program will first wait for the completion of t1 and then t2 . Once, they are finished,
the remaining statements of the current program are executed.
Example:-
import threading
def print_cube(num):
print("Cube: {}" .format(num * num * num))
def print_square(num):
print("Square: {}" .format(num * num))
if __name__ =="__main__":
t1 = threading.Thread(target=print_square, args=(10,))
t2 = threading.Thread(target=print_cube, args=(10,))
t1.start()
t2.start()
t1.join()
t2.join()
print("Done!")
Output
Square: 100
Cube: 1000
Done!
A priority queue is like a regular queue, but each item has a priority. Instead of being served in the order they
arrive, items with higher priority are served first. For example, In airlines, baggage labeled “Business” or
“First Class” usually arrives before the rest.
Key properties of priority queue:
High-priority elements are dequeued before low-priority ones.
If two elements have the same priority, they are dequeued in their order of insertion like a queue.
The multithreaded Queue module is primarily used to manage to process large amounts of data on multiple
threads. It supports the creation of a new queue object that can take a distinct number of items.
The get() and put() methods are used to add or remove items from a queue respectively. Below is the list of
operations that are used to manage Queue:
get(): It is used to add an item to a queue.
put(): It is used to remove an item from a queue.
qsize(): It is used to find the number of items in a queue.
empty(): It returns a boolean value depending upon whether the queue is empty or not.
full(): It returns a boolean value depending upon whether the queue is full or not.
Example:
import queue
import threading
import time
thread_exit_Flag = 0
queueLock.release()
Thread-3 processing C
Thread-3 processing D
Thread-2 processing E
Exiting Thread-2
Exiting Thread-1
Exiting Thread-3
The threading module in Python facilitates the creation and management of threads, enabling concurrent
execution of tasks within a program. A thread represents an independent flow of execution, allowing multiple
operations to occur seemingly simultaneously. While Python's Global Interpreter Lock (GIL) restricts true
parallelism in CPU-bound tasks, threading is beneficial for I/O-bound operations, such as network requests or file
handling, where threads can efficiently utilize waiting times.
Key Features and Concepts
Thread Creation:
The threading.Thread class is used to create new threads. A target function is specified, which the thread will execute
upon starting.
Thread Management:
Methods like start(), join(), and is_alive() control thread execution and synchronization. start() initiates the
thread, join() waits for the thread to complete, and is_alive() checks if the thread is currently running.
Synchronization:
The threading module provides tools for managing shared resources and preventing race conditions, such as locks
(threading.Lock), semaphores (threading.Semaphore), and condition variables ( threading.Condition).
Daemon Threads:
Threads can be set as daemon threads, meaning they will automatically terminate when the main program exits,
regardless of their execution status.
Thread Pools:
The concurrent.futures module offers ThreadPoolExecutor for managing a pool of worker threads, simplifying the execution
of multiple tasks concurrently.
Example
import threading
import time
def task(name):
print(f"Thread {name}: starting")
time.sleep(2)
print(f"Thread {name}: finishing")
if __name__ == "__main__":
threads = []
for i in range(3):
thread = threading.Thread(target=task, args=(i,))
threads.append(thread)
thread.start()
In this example, three threads are created and started, each executing the task function. The join() method ensures
that the main program waits for all threads to finish before exiting.
Multiprocessing
Multiprocessing refers to the ability of a system to support more than one processor at the same time.
Applications in a multiprocessing system are broken to smaller routines that run independently. The
operating system allocates these threads to the processors improving performance of the system.
multiprocessing module which enables the systems to run multiple processes simultaneously. In other words,
developers can break applications into smaller threads that can run independently from their Python code.
These threads or processes are then allocated to the processor by the operating system, allowing them to run in
parallel, improving your Python programs' performance and efficiency. Multiprocessing refers to using multiple
CPUs/processors in a single system. Multiple CPUs can act in a parallel fashion and execute multiple processes together.
A task is divided into multiple processes that run on multiple processors. When the task is over, the results from all
processors are compiled together to provide the final output. Multiprocessing increases the computing power to a great
extent. Symmetric multiprocessing and asymmetric multiprocessing are two types of multiprocessing.
Multiprocessing increases the reliability of the system because of the use of multiple CPUs. However, a significant
amount of time and specific resources are required for multiprocessing. Multiprocessing is relatively more cost effective
as compared to a single CPU system.When we work with Multiprocessing , at first we create process object. Then it calls
a start() method.Consider a computer system with a single processor. If it is assigned several processes at the same
time, it will have to interrupt each task and switch briefly to another, to keep all of the processes going. This situation
is just like a chef working in a kitchen alone. He has to do several tasks like baking, stirring, kneading dough, etc. So
the gist is that: The more tasks you must do at once, the more difficult it gets to keep track of them all, and keeping the
timing right becomes more of a challenge. This is where the concept of multiprocessing arises.
Example
from multiprocessing import Process
def cube(x):
for x in my_numbers:
print('%s cube is %s' % (x, x**3))
if __name__ == '__main__':
my_numbers = [3, 4, 5, 6, 7, 8]
p = Process(target=cube, args=('x',))
p.start()
p.join
print ("Done")
Functions performed by multiprocessing:-
Locks
When we want that only one process is executed at a time in that situation Locks is use. That means that time blocks other
process from executing similar code. Lock will be released after the process gets completed.
Logging
The multiprocessing module also provides logging module to ensure that, if the logging package doesn't use locks
function, the messages between processes mixed up during execution.
Pipes
In multiprocessing, when we want to communicate between processes, in that situation Pipes are used.
Queues
When we pass data between processes then at that time we can use Queue object.
Multiprocessing can be
No such classification present for
Categories classified into symmetric and
multithreading.
asymmetric multiprocessing.
In multiprocessing, many
In multithreading, many threads
Execution processes are executed
are executed simultaneously.
simultaneously.
Multiprocessing requires a
Multithreading requires less time
Resources significant amount of time and
and few resources to create.
large number of resources.
Handling CSV file:-
CSV (Comma Separated Values) is a simple file format used to store tabular data, such as a spreadsheet or database. A
CSV file stores tabular data (numbers and text) in plain text. Each line of the file is a data record. Each record consists
of one or more fields, separated by commas. The use of the comma as a field separator is the source of the name for
this file format. For working CSV files in Python, there is an inbuilt module called CSV.