0% found this document useful (0 votes)
10 views47 pages

Python

Uploaded by

mukisamicross08
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
0% found this document useful (0 votes)
10 views47 pages

Python

Uploaded by

mukisamicross08
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
Download as pdf or txt
You are on page 1/ 47

Python

Lists
Lists are mutable, ordered collections of items.

A list can contain the same data type items or even different data type items.

# How to define a list

lst = []
lst1 = list()

# List containing same data type values


lst = ['a', 'b', 'c']

# List containing different data type values


lst = ['a', 1, 2.14, True]

We can access list elements with the help of indexing.

lst = ['a', 1, 2.14, True]

# This will return the value of the list element which is present in the 0th ind
ex
# This is for returning a specific index
print(lst[0]) # a

Python 1
# Returning the last index of the list
print(lst[-1]) # True

# We can also return the whole list


for i in lst:
print(i)

# Result: a, 1, 2.14, True

# Returning the list of values from a particular index


print(lst[1:3])

# Result: 1, 2.14
# This returns the 1st and 2nd index and not the 3rd index as the first number
"1" indicates that the index must be included and the second number in the brack
et indicates that include the values up to that index but do not include that in
dex

We can change or replace the value in a list at a particular index

lst = ['a', 1, 2.14, True]

# Replacing the 0th index value from "a" to "b"


lst[0] = 'b'

print(lst)

# Result: b, 1, 2.14, True

Some useful functions that are used while the list is being implemented

1. append() - This is used to append a value at the end of the list.

lst = ['a', 1, 2.14, True]

lst.append('b')

print(lst)

# Result: a, 1, 2.14, True, b

Python 2
2. insert() - If we want to insert a value at a specific index then we use insert.

lst = ['a', 1, 2.14, True]

lst.insert(1, 'b')

print(lst)

# Result: a, b, 1, 2.14, True

3. remove() - This is used to remove the first occurrence of a value from a list that is specified.

lst = ['a', 'b', 'a', 1, 2.14, True]

lst.remove('a')

print(lst)

# Result: b, a, 1, 2.14, True

4. pop() - Remove the last element of the list

lst = ['a', 'b', 'a', 1, 2.14, True]

lst.pop()

print(lst)

# Result: a, b, a, 1, 2.14,

5. index() - This is used to fetch the first occurrence of the index of a particular value present in the list

lst = ['a', 1, 2.14, True]

print(lst.index(2.14))

# Result: 2

6. count() - This is used to count the occurrences of values present in the list

Python 3
lst = ['a', 'b', 'a', 1, 2.14, True]

print(lst.count('a'))

# Result: 2

7. sort() - This is used to sort the list values in ascending order.

lst = ['a', 'c', 'b']

lst.sort()

print(lst)

# Result: a, b, c

8. reverse() - This is used to sort the list values in descending order.

lst = ['a', 'c', 'b']

lst.reverse()

print(lst)

# Result: c, b, a

9. clear() - This is used to remove all the values in the list.

lst = ['a', 'c', 'b']

lst.clear()

print(lst)

# Result: Empty List

Some shortcuts for using a list

# Return the square of the number from 1 - 9


for i in range(10):
print(i**2)

Python 4
# Optimized code would be
lst = [i**2 for i in range(10)]

# Now we only require the square root of even numbers


for i in range(10):
if i%2 == 0:
print(i**2)

# Optimized code would be - Using an if condition


lst = [i**2 for i in range(10) i%2 == 0]

Tuple
Tuples are immutable, ordered collections of items

Tuples can contain the same data type items or even different data type items.

The main difference between a list and a tuple is that lists are mutable and tuples are immutable.

# How to define a tuple

tpl1 = tuple()

# Tuple containing same data type values


tpl = ('a', 'b', 'c')

# Tuple containing different data type values


tpl = ('a', 1, 2.14, True)

We can access tuple elements with the help of indexing.

tpl = ('a', 1, 2.14, True)

# This will return the value of the tuple element which is present in the 0th in
dex
# This is for returning a specific index
print(tpl[0]) # a

# Returning the last index of the tuple

Python 5
print(tpl[-1]) # True

# We can also return the whole tuple


for i in tpl:
print(i)

# Result: a, 1, 2.14, True

# Returning the tuple of values from a particular index


print(tpl[1:3])

# Result: 1, 2.14
# This returns the 1st and 2nd index and not the 3rd index as the first number
"1" indicates that the index must be included and the second number in the brack
et indicates that include the values up to that index but do not include that in
dex

Some useful functions that are used while the list is being implemented

1. Tuple concatenation - We can concatenate 2 tuples to return a single tuple

tpl1 = (1,2,3,4,5)
tpl2 = ('a','b')

tpl3 = tpl1 + tpl2

print(tpl3)

# Result: 1, 2, 3, 4, 5, a, b

2. count() - This counts the occurrences of a particular value

tpl1 = (1,2,3,4,5,1,1,2)

print(tpl.count(1))

# Result: 3

3. index() - This is used to return the first occurrence index of a specified value

tpl1 = (1,2,3,4,5,1,1,2)

Python 6
print(tpl.index(1))

# Result: 0

4. Packed tuple - By default if we haven’t specified any method then by default it is set as a tuple

tpl1 = 1,2,3,4

print(type(tpl1))

# Result: tuple

5. Unpacked tuple - We can assign individual variables to individual values in a tuple

tpl1 = 1,2,3,4

a,b,c,d = tpl1

print(a) # 1
print(b) # 2
print(c) # 3
print(d) # 4

first, second, *third = tpl1


print(first) # 1
print(second) # 2
print(third) # (3,4)

Sets
Sets are used to store unique elements in an unordered manner.

Sets can contain the same data type items or even different data type items.

# How to define a set

st = ()
st1 = set()

# Set containing same data type values


st = {'a', 'b', 'c'}

Python 7
# Set containing different data type values
st = {'a', 1, 2.14, False}

We can access set elements with the help of indexing.

st = {'a', 1, 2.14, True}

# This will return the value of the set element that is present in the 0th index
# This is for returning a specific index
print(st[0]) # a

# Returning the last index of the set


print(st[-1]) # True

# We can also return the whole set


for i in st:
print(i)

# Result: a, 1, 2.14, True

# Returning the set of values from a particular index


print(st[1:3])

# Result: 1, 2.14
# This returns the 1st and 2nd index and not the 3rd index as the first number
"1" indicates that the index must be included and the second number in the brack
et indicates that include the values up to that index but do not include that in
dex

Some useful functions used while the list is being implemented

1. add() - It is used to add a value at the last index of the set. Remember if that particular value that is
being added is already present in the set then the value will not be added as a set only stores unique
values.

st = {1,2,3,4}

st.add(5)

print(st) # 1,2,3,4,5

Python 8
st.add(1) # Does not add the value 1 in the set because the value already exists
in the set

print(st) # 1,2,3,4,5

2. remove() - This is used to remove a particular value from the set.

st = {1,2,3,4}

st.remove(3)

print(st) # 1,2,4

3. discard() - This is the same as the remove() but the only difference is that suppose we have a value
that we want to remove in the set but that value is not present in the set, so remove() will prompt us an
error but discard will not prompt us an error.

st = {1,2,3,4}

st.discard(5)

print(st) # 1,2,3,4

4. pop() - This is used to remove the value that is present in the last index of the set.

st = {1,2,3,4}

st.pop()

print(st) # 1,2,3

5. clear() - This is used to remove all the values present in the set.

st = {1,2,3,4}

st.clear()

print(st) # Empty Set

6. in - This is used to check whether a particular value is present in the set or not

Python 9
st = {1,2,3,4}

print(1 in st) # Returns True

print(10 in st) # Returns False

7. union() - This is used to union 2 sets together

st1 = {1,2,3,4}
st2 = {7,8,9,10}

print(set1.union(set2))

# Result: {1,2,3,4,7,8,9,10}

8. intersection() - This is used to return those values which are present in both the sets

st1 = {1,2,3,4,9}
st2 = {1,7,8,9,10}

print(set1.intersection(set2))

# Result: {1,9}

9. intersection_update() - This is used to return those values that are present in both sets and also update
the first set which is used before the intersection_update() function.

st1 = {1,2,3,4,9}
st2 = {1,7,8,9,10}

print(set1.intersection_update(set2))
# Result: {1,9}

print(set1)
# Result: {1,9}

10. difference() - This is used to return the elements which are present in the first set and not present in
the second set.

st1 = {1,2,3,4,9}
st2 = {1,7,8,9,10}

Python 10
print(set1.differnce(set2))
# Result: {2,3,4}

11. symmetric_difference() - This is used to return the elements which are not common in both sets.

st1 = {1,2,3,4,9}
st2 = {1,7,8,9,10}

print(set1.symmetric_difference(set2))
# Result: {2,3,4,7,8,10}

12. issubset() - This returns True or False whether the set is a subset of another set or not

st1 = {1,2,3,4,9}
st2 = {1,2,3,4,7,8,9,10}

print(set1.issubset(set2))
# Result: True

st1 = {1,2,3,4,9}
st2 = {1,7,8,9,10}

print(set1.issubset(set2))
# Result: False

13. superset() - This returns True or False whether a set is a superset of another set or not. This is the
opposite of issubset().

Dictionary
Dictionaries are an unordered collection of items.

They store the data in the form of key-value pairs and each key must be unique and immutable.

# How to define a dictionary

my_dict = {}
my_dict = dict()

# Example of key-value pair dictionary


my_dict = {

Python 11
"name": "Alice",
"age": 30,
"city": "New York"
}

# Accessing a value from a dictionary with the help of a key


print(my_dict["name"]) # Output: Alice

# Inserting a new key-value in the dictionary


my_dict["email"] = "alice@example.com"

# Replacing a value in an already existing key


my_dict["age"] = 31

# Removing a key-value pair


del my_dict["city"]

Accessing all the key-value pairs from a dictionary

for key, value in my_dict.items():


print(f"{key}: {value}")

# Result
# name: Alice
# age: 31
# email: alice@example.com

Some useful functions used while the list is being implemented

1. copy() - This is used to copy the exisiting dictionary key-values into another variable whose type will
become a dictionary.

# Original dictionary
original_dict = {
"name": "Alice",
"age": 30,
"city": "New York"
}

# Creating a shallow copy of the dictionary


copied_dict = original_dict.copy()

Python 12
# Modifying the copied dictionary
copied_dict["age"] = 31

# Displaying both dictionaries


print("Original dictionary:", original_dict)
print("Copied dictionary:", copied_dict)

# Result:
# Original dictionary: {'name': 'Alice', 'age': 30, 'city': 'New York'}
# Copied dictionary: {'name': 'Alice', 'age': 31, 'city': 'New York'}

2. Fetching only the keys from a dictionary

# Example dictionary
my_dict = {
"name": "Alice",
"age": 30,
"city": "New York"
}

# Fetching the keys


keys = my_dict.keys()

# Converting the keys view object to a list (if needed)


keys_list = list(keys)

# Displaying the keys


print(keys) # Result: dict_keys(['name', 'age', 'city'])
print(keys_list) # Result: ['name', 'age', 'city']

3. Fetching only the values from a dictionary

# Example dictionary
my_dict = {
"name": "Alice",
"age": 30,
"city": "New York"
}

# Fetching the values


values = my_dict.values()

# Converting the values view object to a list (if needed)


values_list = list(values)

Python 13
# Displaying the values
print("Result:", values) # Result: dict_values(['Alice', 30, 'New York'])
print("Result:", values_list) # Result: ['Alice', 30, 'New York']

4. Merging 2 dictionaries

# Example dictionaries
dict1 = {"name": "Alice", "age": 30}
dict2 = {"city": "New York", "email": "alice@example.com"}

# Merging dictionaries using ** unpacking


merged_dict = {**dict1, **dict2}

# Result
print("Result:", merged_dict)
# Result: {'name': 'Alice', 'age': 30, 'city': 'New York', 'email': 'alice@examp
le.com'}

Function
A function is a block of code which can be used for many purposes like code reusability, orgainizing
code and imporve readability.

# Defining a function with a sigle parameter


def greet(name):
"""
This function greets the person whose name is passed as an argument.
"""
greeting = f"Hello, {name}!"
return greeting

# Calling the function


result = greet("Alice")

# Displaying the result


print("Result:", result)
# Result: Hello, Alice!

# def is the keyword to define a function.


# greet is the function name.
# name is the parameter the function accepts.
# We call the greet function with the argument "Alice".

Python 14
# The returned value is stored in the result variable.
# We print the result to see the output of the function call.

A function with multiple parameters

# Defining a function
def add_numbers(a, b):
"""
This function returns the sum of two numbers.
"""
return a + b

# Calling the function


sum_result = add_numbers(5, 3)

# Displaying the result


print("Result:", sum_result)
# Result: 8

# The add_numbers function takes two parameters a and b.


# It returns the sum of a and b.
# We call the function with the arguments 5 and 3, and print the result.

A function with default parameter

# Defining a function with a default parameter


def greet(name, greeting="Hello"):
"""
This function greets the person with the provided greeting.
If no greeting is provided, it uses "Hello" by default.
"""
return f"{greeting}, {name}!"

# Calling the function without the default parameter


result1 = greet("Alice")

# Calling the function with the default parameter


result2 = greet("Bob", "Hi")

# Displaying the results


print("Result 1:", result1) # Result 1: Hello, Alice!
print("Result 2:", result2) # Result 2: Hi, Bob!

Args - In Python, *args is used in function definitions to allow the function to accept a variable
number of positional arguments. This means you can pass any number of arguments to the

Python 15
function, and they will be accessible as a tuple within the function.

# Defining a function with *args


def sum_numbers(*args):
"""
This function takes any number of numeric arguments and returns their sum.
"""
total = sum(args) # Using the built-in sum function to calculate the tota
l
return total

# Calling the function with multiple arguments


result1 = sum_numbers(1, 2, 3)
result2 = sum_numbers(10, 20, 30, 40)

# Displaying the results


print("Result 1:", result1) # Result 1: 6
print("Result 2:", result2) # Result 2: 100

Kwargs - In Python, **kwargs is used in function definitions to allow the function to accept a variable
number of keyword arguments. This means you can pass any number of arguments in the form of
key-value pairs, and they will be accessible as a dictionary within the function.

# Defining a function with **kwargs


def display_info(**kwargs):
"""
This function accepts any number of keyword arguments
and displays them as key-value pairs.
"""
for key, value in kwargs.items():
print(f"{key}: {value}")

# Calling the function with multiple keyword arguments


display_info(name="Alice", age=30, city="New York", email="alice@example.com")

Using both *Args and **Kwargs together in a function.

# Defining a function with *args and **kwargs


def display_data(*args, **kwargs):
"""
This function accepts any number of positional and keyword arguments
and displays them.
"""
# Displaying positional arguments
print("Positional arguments:")

Python 16
for arg in args:
print(f"- {arg}")

# Displaying keyword arguments


print("\nKeyword arguments:")
for key, value in kwargs.items():
print(f"{key}: {value}")

# Calling the function with both positional and keyword arguments


display_data("Alice", 30, city="New York", email="alice@example.com")

# Result
# Positional arguments:
# Alice
# 30

# Keyword arguments:
# city: New York
# email: alice@example.com

Lambda Function - A lambda function in Python is a small anonymous function defined using the
lambda keyword. Lambda functions can have any number of arguments but only one expression.
The expression is evaluated and returned. They are often used for short, simple operations where
defining a full function would be overkill.

# Syntax
lambda arguments: expression

# Defining a lambda function


add = lambda x, y: x + y

# Using the lambda function


result = add(5, 3)

# Displaying the result


print("Result:", result) # Result: 8

Map Function - The map() function in Python applies a given function to all the items in an input list
(or any iterable) and returns a map object (which is an iterator) of the results. You can convert the
map object to a list, tuple, or other collection types if needed.

# Syntax
map(function, iterable, ...)

# Defining a function to square a number


def square(x):

Python 17
return x ** 2

# List of numbers
numbers = [1, 2, 3, 4, 5]

# Using map() to apply the square function to each element in the numbers list
squared_numbers = map(square, numbers)

# Converting the map object to a list


squared_numbers_list = list(squared_numbers)

# Displaying the result


print("Squared numbers:", squared_numbers_list)
# Squared numbers: [1, 4, 9, 16, 25]

# Using the above example but using the lambda function to return the square r
oot of each value

# List of numbers
numbers = [1, 2, 3, 4, 5]

# Using map() with a lambda function to square each number


squared_numbers = map(lambda x: x ** 2, numbers)

# Converting the map object to a list


squared_numbers_list = list(squared_numbers)

# Displaying the result


print("Squared numbers:", squared_numbers_list)
# Squared numbers: [1, 4, 9, 16, 25]

So from above we see that it is better to use lambda functions as compared to creating functions as
they are simple calculations

Filter Function - The filter() function in Python is used to construct an iterator from elements of
an iterable for which a function returns true. In other words, it filters the elements of the iterable
based on a condition provided by a function.

# Syntax
filter(function, iterable)

# List of numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Using filter() with a lambda function to filter even numbers

Python 18
even_numbers = filter(lambda x: x % 2 == 0, numbers)

# Converting the filter object to a list


even_numbers_list = list(even_numbers)

# Displaying the result


print("Even numbers:", even_numbers_list)
# Even numbers: [2, 4, 6, 8, 10]

removes elements that do not satisfy the provided function's condition while a
filter() map()

transforms each element in the iterable using the provided function.

# List of numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Using filter() to get only even numbers


even_numbers = filter(lambda x: x % 2 == 0, numbers)

# Using map() to square each even number


squared_even_numbers = map(lambda x: x ** 2, even_numbers)

# Converting the map object to a list


squared_even_numbers_list = list(squared_even_numbers)

# Displaying the result


print("Squared even numbers:", squared_even_numbers_list)
# Squared even numbers: [4, 16, 36, 64, 100]

# Use filter() when you want to remove items from an iterable based on a condi
tion.
# Use map() when you want to transform items in an iterable.
# They can be combined for more complex operations, such as filtering and then
transforming the items.

File Handling
File handling in Python involves working with files to read from or write to them. Python provides built-
in functions and methods to handle files easily.

1. Opening a file

'r' : Read (default)

'w' : Write (creates a new file or truncates the existing file)

Python 19
'a' : Append (writes data to the end of the file)

'b' : Binary mode

't' : Text mode (default)

'+' : Read and write

seek(): This function points back to the first line and should be used when we are preforming both
write as well as read operations.

1. Opening a file

# Opens a file named example.txt in write mode. If the file doesn't exist, it is
created

file = open("example.txt", "w")

2. Writing to a file

# Writes two lines to the file

file.write("Hello, world!\n")
file.write("This is a file handling example.\n")

3. Closing a file

# Closes the file to ensure the data is saved and resources are freed

file.close()

Using the above 3 point we are going to write a code that opens a file, writes some text in it and then
closes the file.

# Opening a file in write mode


file = open("example.txt", "w")

# Writing to the file


file.write("Hello, world!\n")
file.write("This is a file handling example.\n")

# Closing the file


file.close()

Python 20
4. Reading from a file

# Opening a file in read mode


file = open("example.txt", "r")

# Reading the entire content of the file


content = file.read()

# Printing the content


print("File content:")
print(content)

# Closing the file


file.close()

5. Using with statements - Using the with statement is a preferred way to handle files because it
ensures that the file is properly closed after its suite finishes, even if an exception is raised.

# Writing to a file using with statement


with open("example.txt", "w") as file:
file.write("Hello, world!\n")
file.write("This is a file handling example.\n")

# Reading from a file using with statement


with open("example.txt", "r") as file:
content = file.read()
print("File content:")
print(content)

6. Appending data to a file

# Appending to a file
with open("example.txt", "a") as file:
file.write("Appending a new line.\n")

# Reading the updated file


with open("example.txt", "r") as file:
content = file.read()
print("Updated file content:")
print(content)

Use open() to open a file in various modes ( 'r' , 'w' , 'a' , etc.).

Use file.read() to read the content of a file.

Python 21
Use file.write() to write to a file.

Use file.close() to close the file when done.

Use the with statement to ensure that the file is properly closed after its block of code is executed.

Working with OS package which can be imported from the python library.

import os

Some of the functions that are most commonly used in the os packages are:-

1. getcwd() - This is used to get the current working directory.

print(os.getcwd())

2. mkdir() - This is used to create a new directory.

# Let the name of the new directory be dir1


print(ok.mkdir('dir1'))

3. listdir() - This is used to list all the files and directories present in our current path.

print(os.listdir('.'))

4. join() - This is used to join paths if the path of a file is present in the form of a list or a string.

dir_name "dir1"
file_name = "file1"
full_path = os.path.join(os.getcwd(), dir_name, file_name)

print(full_path)

5. exists() - This is used to check whether a file path exists or not.

dir_name "dir1"
file_name = "file1"
full_path = os.path.join(os.getcwd(), dir_name, file_name)

print(os.path.exists(full_path)) # Retruns True if exists else False

Python 22
6. isfile() - This is used to check whether the given path is a file or not.

7. isdir() - This is used to check whether the given path is a directory or not.

8. Relative Path - This is basically the file name.

9. abspath() - This is used to get the complete path from the relative path, we should make sure that the
relative path that we are specifying must not have 2 same file names. This is also called as the absolute
path .

Exception Handling
Exception handling in Python is done using the try , except , else , and finally blocks. This mechanism
allows you to handle errors gracefully without crashing your program.

# Syntax

try:
# Code that might raise an exception
pass
except ExceptionType:
# Code that runs if an exception occurs
pass
else:
# Code that runs if no exception occurs
pass
finally:
# Code that runs no matter what (even if an exception occurs or not)
pass

Some of the common exception errors that we recieve are:-

1. ZeroDivisionError - Dividing by zero.

2. FileNotFoundError - File not found.

3. ValueError - Inavalid value.

4. TypeError - Invalid type.

5. NameError - A particular name or a varibale is not defined.

6. Exception - This contains most of the common error in a class. (We should preferably be using this as
comapred to some common errors) [This should be written in the last of all the exceptions as it is the
parent of exceptions]

def divide(a, b):


try:
result = a / b

Python 23
except ZeroDivisionError as e:
print(f"Error: Division by zero is not allowed. Exception: {e}")

except TypeError as e:
print(f"Error: Unsupported operand type(s). Exception: {e}")

except Exception as e:
print(f"Error: Unsupported operand type(s). Exception: {e}")

else:
print(f"The result is {result}")

finally:
print("Execution completed.")

# Test cases
divide(10, 2) # Normal case
divide(10, 0) # Division by zero
divide(10, "a") # Unsupported operand type

Classes and Objects


A class is a blueprint for creating objects. It defines a set of attributes and methods that the created
objects can use. In Python, you define a class using the class keyword.

class Dog:
# Class attribute
species = "Canis familiaris"

# Initializer / Instance attributes


def __init__(self, name, age):
self.name = name
self.age = age

# Instance method
def description(self):
return f"{self.name} is {self.age} years old"

# Another instance method


def speak(self, sound):
return f"{self.name} says {sound}"

An object is an instance of a class. It is a specific realization of the class with actual values for the
attributes defined in the class.

Python 24
# You create an object by calling the class as if it were a function
my_dog = Dog("Buddy", 3)

Accessing the attributes and methods - We can access the attributes and methods of an object using
dot notation.

print(my_dog.name) # Output: Buddy


print(my_dog.age) # Output: 3
print(my_dog.species) # Output: Canis familiaris

print(my_dog.description()) # Output: Buddy is 3 years old


print(my_dog.speak("Woof")) # Output: Buddy says Woof

# Class Attribute: species is shared by all instances of the class.


# Instance Attributes: name and age are unique to each instance.
# Initializer (__init__ method): This special method is called when an instance
of the class is created. It initializes the instance attributes.
# Instance Methods: description and speak are methods that operate on the instan
ce attributes.

Here self is a instance variable, now we’ll be looking into instance methods

class Car:
def __init__(self, make, model, year, color):
self.make = make
self.model = model
self.year = year
self.color = color
self.odometer_reading = 0

def get_descriptive_name(self):
return f"{self.year} {self.make} {self.model}"

def read_odometer(self):
return f"This car has {self.odometer_reading} miles on it."

def update_odometer(self, mileage):


if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")

def increment_odometer(self, miles):

Python 25
self.odometer_reading += miles

# Creating an object
my_car = Car("Toyota", "Corolla", 2020, "Blue")

# Accessing attributes and methods


print(my_car.get_descriptive_name()) # Output: 2020 Toyota Corolla
print(my_car.read_odometer()) # Output: This car has 0 miles on it.

# Updating the odometer


my_car.update_odometer(1500)
print(my_car.read_odometer()) # Output: This car has 1500 miles on it.

# Incrementing the odometer


my_car.increment_odometer(500)
print(my_car.read_odometer()) # Output: This car has 2000 miles on it.

# Instance Attribute (odometer_reading): Initialized to 0 and updated using methods


# Instance Methods (update_odometer and increment_odometer): Used to safely modify

To summarise classes and objects:-

Class: A blueprint for creating objects with attributes and methods.

Object: An instance of a class with actual values for the attributes.

Attributes: Variables that belong to a class or an instance.

Methods: Functions that belong to a class or an instance.

__init__ Method: A special method called when an object is instantiated. It initializes the instance
attributes.

Inheritance
Inheritance is a fundamental concept in object-oriented programming (OOP) that allows one class (the
child or derived class) to inherit attributes and methods from another class (the parent or base class).
This promotes code reuse and logical hierarchy.

Single inheritance is a form of inheritance where a class (child or derived class) inherits from only one
parent (base) class. This is the simplest form of inheritance and helps in reusing the code of the parent
class in the child class.

# Parent Class
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model

Python 26
def start_engine(self):
return f"The engine of the {self.brand} {self.model} is starting."

def stop_engine(self):
return f"The engine of the {self.brand} {self.model} is stopping."

# Child Class
class Car(Vehicle):
def __init__(self, brand, model, num_doors):
super().__init__(brand, model) # Initialize the parent class
self.num_doors = num_doors # Initialize the child class attribute

def play_music(self, song):


return f"Playing {song} in the {self.brand} {self.model}."

# Creating an object of the Car class


my_car = Car("Toyota", "Corolla", 4)

print(my_car.start_engine()) # Output: The engine of the Toyota Corolla is start


print(my_car.play_music("Shape of You")) # Output: Playing Shape of You in the Toy
print(my_car.stop_engine()) # Output: The engine of the Toyota Corolla is stopp
print(f"The car has {my_car.num_doors} doors.") # Output: The car has 4 doors.

1. Parent Class ( Vehicle ):

Initialiser ( __init__ method): Sets the brand and model attributes.

Methods ( start_engine and stop_engine ): Provide functionality related to the vehicle's engine.

2. Child Class ( Car ):

Initializer ( __init__ method): Calls the parent class's __init__ method using super().__init__(brand,
model) to initialize brand and model . It also initializes a new attribute num_doors .

New Method ( play_music ): Adds additional functionality specific to cars.

3. Creating Objects:

An instance of Car is created with brand , model , and num_doors .

The start_engine , stop_engine , and play_music methods are available in the Car class.

The Car class also has the num_doors attribute initialized properly.

Why Use super()


Code Reusability: Reuses the initialization code of the parent class.

Maintainability: Makes the code easier to maintain by ensuring the parent class is properly initialized.

Flexibility: Allows the child class to add additional initialization code specific to its needs.

Python 27
Multiple inheritance is a feature in object-oriented programming where a class can inherit attributes
and methods from more than one parent class. This allows the child class to utilise the functionality of
multiple parent classes.

# Parent Class 1
class Engine:
def __init__(self, engine_type):
self.engine_type = engine_type

def start_engine(self):
return f"Starting {self.engine_type} engine."

# Parent Class 2
class Body:
def __init__(self, body_type):
self.body_type = body_type

def body_info(self):
return f"This is a {self.body_type} body."

# Child Class
class Car(Engine, Body):
def __init__(self, engine_type, body_type, brand):
Engine.__init__(self, engine_type)
Body.__init__(self, body_type)
self.brand = brand

def car_info(self):
return f"This is a {self.brand} car with a {self.engine_type} engine and a

# Object creation
my_car = Car("V8", "Sedan", "Toyota")

print(my_car.start_engine()) # Output: Starting V8 engine.


print(my_car.body_info()) # Output: This is a Sedan body.
print(my_car.car_info()) # Output: This is a Toyota car with a V8 engine and

Polymorphism
Polymorphism is a key concept in object-oriented programming that refers to the ability of different
classes to be treated as instances of the same class through a common interface. This means that a
single function or method can work in different ways depending on the object it is acting upon.
Common in scenarios where you want to enforce a certain interface across multiple related classes,
such as in payment processing, file handling, or UI components.

Python 28
Method Overriding (Runtime Polymorphism): Achieved through inheritance, where a child class
provides a specific implementation of a method that is already defined in its parent class.

# Parent Class
class Animal:
def speak(self):
return "Animal sound"

# Child Classes
class Dog(Animal):
def speak(self):
return "Woof!"

class Cat(Animal):
def speak(self):
return "Meow!"

# Function Demonstrating Polymorphism


def make_animal_speak(animal):
print(animal.speak())

# Creating objects of different classes


dog = Dog()
cat = Cat()

# Using the same function to handle different objects


make_animal_speak(dog) # Output: Woof!
make_animal_speak(cat) # Output: Meow!

Parent Class ( Animal ):

Defines a generic speak method that returns "Animal sound".

Child Classes ( Dog and Cat ):

Both override the speak method of the Animal class to provide specific sounds for dogs and cats.

Polymorphic Function ( make_animal_speak ):

Takes an Animal object (or any object that has a speak method) as an argument and calls the speak

method.

Depending on the object passed ( Dog or Cat ), the method behaves differently, demonstrating
polymorphism.

Some of the uses of Polymorphism are:-

Python 29
1. Flexibility: Allows a single function to work with objects of different types.

2. Code Reusability: You can write more generic code that can work with various types of objects
without modification.

3. Ease of Maintenance: Reduces redundancy by enabling a single method to work for various object
types.

Abstract Classes

An abstract class in Python is a class that cannot be instantiated on its own and is meant to be
subclassed. Abstract classes allow you to define a common interface for a group of related
classes, ensuring that all subclasses implement certain methods. Abstract classes serve as a
blueprint for other classes.

Abstract Method: A method that is declared in an abstract class but has no implementation.
Subclasses must override this method.

Abstract Class: A class containing one or more abstract methods. You cannot instantiate abstract
classes directly.

from abc import ABC, abstractmethod

# Abstract Class
class PaymentProcessor(ABC):
@abstractmethod
def authorize_payment(self, amount):
pass

@abstractmethod
def process_payment(self, amount):
pass

# Derived Classes
class CreditCardProcessor(PaymentProcessor):
def authorize_payment(self, amount):
return f"Authorizing credit card payment of {amount}."

def process_payment(self, amount):


return f"Processing credit card payment of {amount}."

class PayPalProcessor(PaymentProcessor):
def authorize_payment(self, amount):
return f"Authorizing PayPal payment of {amount}."

def process_payment(self, amount):


return f"Processing PayPal payment of {amount}."

Python 30
# Function Demonstrating Polymorphism
def execute_payment(processor, amount):
print(processor.authorize_payment(amount))
print(processor.process_payment(amount))

# Using CreditCardProcessor
cc_processor = CreditCardProcessor()
execute_payment(cc_processor, 100.00)

# Using PayPalProcessor
paypal_processor = PayPalProcessor()
execute_payment(paypal_processor, 50.00)

Abstract Class ( PaymentProcessor ):

Defines a template for all payment processors with authorize_payment and process_payment

methods.

These methods are abstract, meaning subclasses must implement them.

Concrete Subclasses ( CreditCardProcessor and PayPalProcessor ):

Both implement the authorize_payment and process_payment methods, each in a way that suits their
specific payment method.

Usage:

You can pass different payment processors to the execute_payment function, and it will work
seamlessly with any class that follows the PaymentProcessor blueprint.

Class Method

A class method is a method that is bound to the class and not the instance of the class. Class
methods can modify class state that applies across all instances of the class, and they have access
to the class itself (not the instance). They are defined using the @classmethod decorator and
take cls (referring to the class) as their first parameter instead of self .

Class methods are defined using the @classmethod decorator, and the first argument must
be cls (which stands for the class itself).

class Employee:
# Class attribute
raise_percentage = 1.05

def __init__(self, name, salary):


self.name = name
self.salary = salary

# Class method to update the raise percentage


@classmethod
def set_raise_percentage(cls, new_percentage):

Python 31
cls.raise_percentage = new_percentage

# Class method to create an Employee instance from a string


@classmethod
def from_string(cls, emp_string):
name, salary = emp_string.split('-')
return cls(name, float(salary))

def apply_raise(self):
self.salary *= self.raise_percentage

# Using the class method to modify class attribute


Employee.set_raise_percentage(1.10)

# Creating an Employee instance using a class method


emp1 = Employee.from_string('John-50000')

# Applying raise to the employee


emp1.apply_raise()

print(emp1.name) # Output: John


print(emp1.salary) # Output: 55000.0 (50000 * 1.10)

Explanation of the Code:


1. Class Attribute: raise_percentage is a class-level attribute that applies to all instances of Employee .

2. @classmethod Decorator: This defines set_raise_percentage and from_string as class methods.

3. set_raise_percentage(cls, new_percentage) : This class method modifies the class-level


attribute raise_percentage for all instances of the class. It uses cls to refer to the class itself.

4. from_string(cls, emp_string) : This is an alternative constructor that takes a string (like 'John-50000' ),
splits it, and creates a new Employee instance.

5. Calling the Class Method: We call Employee.set_raise_percentage(1.10) to set the raise percentage for
all employees.

6. Using Class Method for Alternative Constructor: The from_string method allows creating
an Employee object from a string without having to parse the string manually outside the class.

You use class methods when you need to work with class-level data, or when you want to provide
alternative constructors that operate on the class itself rather than instances. A common use case
is to provide functionality that applies to the class as a whole, rather than specific instances.

Static Method

In Python, a staticmethod is a method within a class that does not receive an implicit first argument
like self (which is for instance methods) or cls (which is for class methods). This means

Python 32
a staticmethod does not have access to the instance or class itself, but it can still be called on the
class or an instance of the class.

class MathUtils:
@staticmethod
def add(a, b):
return a + b

# Using the static method


result = MathUtils.add(5, 3)
print(result) # Output: 8

Explanation of the Code:


1. Definition of @staticmethod : The @staticmethod decorator is used to define a static method within a
class. It tells Python that the method does not need access to the instance ( self ) or the class
( cls ).

2. Method Definition: The add method is defined with the @staticmethod decorator. It only takes
parameters a and b , and it performs a simple addition operation.

3. Calling the Static Method: You can call the static method using the class name MathUtils.add(5, 3) .
This is because static methods are not tied to a particular instance of the class.

4. Output: The method returns the result of the addition, which is then printed out.

Utility Functions: Use static methods for utility functions that perform operations independent of
the class or instance.

@Property

The @property decorator in Python is used to define methods in a class that act like attributes. This
allows you to create managed attributes where you can control how they are accessed and
modified, while keeping the syntax clean and intuitive.

class Rectangle:
def __init__(self, length, width):
self.__length = length
self.__width = width

@property
def length(self):
return self.__length

@length.setter
def length(self, length):
self.__length = length

Python 33
@property
def width(self):
return self.__width

@width.setter
def width(self, width):
self.__width = width

Private Attributes: __length and __width are private attributes intended to be inaccessible directly
from outside the class. This is a form of encapsulation that helps protect the internal state of the
object.

Properties: The @property decorator is used to define getter methods for length and width , allowing
you to access these attributes in a controlled manner. The corresponding setter methods allow you
to update these attributes while maintaining encapsulation.

Encapsulation: Using @property allows you to control how attributes are accessed and modified,
even though they are private. This approach ensures that the internal representation is hidden from
outside the class, while still providing a clear and intuitive interface for interacting with these
attributes.

Encapsulation
Encapsulation is one of the fundamental concepts in object-oriented programming. It refers to the
bundling of data (attributes) and methods (functions) that operate on the data into a single unit, or
class. Encapsulation also restricts direct access to some of the object’s components, preventing
accidental interference and misuse of the data.

In Python, encapsulation is implemented through access modifiers, which control the visibility of class
members (attributes and methods). The three levels of access control are:

1. Public: Accessible from anywhere. These attributes are accessible from outside the class. They can be
accessed directly using the object of the class.

class Car:
def __init__(self, make, model, year):
self.make = make # Public attribute
self.model = model # Public attribute
self.year = year # Public attribute

def display_info(self):
return f"Car: {self.year} {self.make} {self.model}"

# Creating an object of the Car class


my_car = Car("Toyota", "Corolla", 2020)

Python 34
# Accessing public attributes directly
print(my_car.make) # Output: Toyota
print(my_car.model) # Output: Corolla
print(my_car.year) # Output: 2020

# Modifying public attributes directly


my_car.year = 2021
print(my_car.display_info()) # Output: Car: 2021 Toyota Corolla

In the Car class, make , model , and year are public attributes because they are defined without any
leading underscores. This means they can be accessed and modified directly from outside the class,
as shown in the example.

2. Private: Accessible only within the class itself. Private attributes in Python are a way to prevent direct
access to class variables from outside the class. This is a form of encapsulation, which restricts
access to certain components, ensuring that data is not modified unintentionally.

In Python, private attributes are created by prefixing the attribute name with two underscores ( __ ).

class Car:
def __init__(self, make, model, year):
self.__make = make # Private attribute
self.__model = model # Private attribute
self.__year = year # Private attribute

def display_info(self):
return f"Car: {self.__year} {self.__make} {self.__model}"

def set_year(self, year):


if year > 1885: # Validation check for the year
self.__year = year
else:
print("Invalid year!")

def get_year(self):
return self.__year

# Creating an object of the Car class


my_car = Car("Toyota", "Corolla", 2020)

# Accessing private attributes directly (This will raise an AttributeError)


# print(my_car.__make) # This would raise an AttributeError

# Accessing private attributes via public methods


print(my_car.get_year()) # Output: 2020

Python 35
# Modifying private attributes via public methods
my_car.set_year(2021)
print(my_car.display_info()) # Output: Car: 2021 Toyota Corolla

# Attempting to set an invalid year


my_car.set_year(1800) # Output: Invalid year!

Private Attributes: In the Car class, __make , __model , and __year are private attributes. The double
underscore ( __ ) at the beginning of the attribute name makes it private. This means that these
attributes cannot be accessed or modified directly from outside the class.

Encapsulation: By making attributes private, you encapsulate the internal state of the object, providing
controlled access via public methods ( get_year , set_year ). This ensures that the data is validated or
processed before being modified, preventing accidental or inappropriate changes.

Accessing Private Attributes: Attempting to access __make directly using my_car.__make will raise an
AttributeError , as these attributes are not accessible directly. Instead, public methods like get_year()

and set_year() are provided to interact with the private attributes.

Name Mangling: Python internally changes the name of the private attribute to make it harder to
access them from outside. For example, __make is internally changed to _Car__make . However, this is not
intended for regular access but rather to prevent accidental modifications.

Benefits of Private Encapsulation

Data Integrity: Ensures that only valid data is assigned to attributes.

Encapsulation: Hides the internal representation of the object from the outside world.

Controlled Access: Allows for controlled access through getter and setter methods.

3. Protected: Accessible within the class and its subclasses. Protected attributes in Python are intended
to be accessed within the class itself and by subclasses, but not from outside these classes.

In Python, a protected attribute is indicated by prefixing the attribute name with a single underscore
( _ ).

class Car:
def __init__(self, make, model, year):
self._make = make # Protected attribute
self._model = model # Protected attribute
self._year = year # Protected attribute

def display_info(self):
return f"Car: {self._year} {self._make} {self._model}"

# Subclass inheriting from Car


class ElectricCar(Car):
def __init__(self, make, model, year, battery_capacity):

Python 36
super().__init__(make, model, year)
self._battery_capacity = battery_capacity # Additional protected attribute

def display_battery_info(self):
return f"{self._make} {self._model} has a {self._battery_capacity} kWh batt

# Creating an object of the ElectricCar class


my_electric_car = ElectricCar("Tesla", "Model S", 2021, 100)

# Accessing protected attributes directly from the subclass


print(my_electric_car.display_battery_info()) # Output: Tesla Model S has a 100 kW

# Accessing protected attributes directly from the base class


print(my_electric_car.display_info()) # Output: Car: 2021 Tesla Model S

# Accessing protected attributes directly from outside (not recommended)


print(my_electric_car._make) # Output: Tesla

Implementing all three types of Polymorphism which are Private, Protected and Public methods and
attributes

class BankAccount:
def __init__(self, account_holder, initial_balance):
# Public attribute
self.account_holder = account_holder
# Protected attribute (typically should not be accessed directly outside th
self._balance = initial_balance
# Private attribute (intended to be accessed only within the class)
self.__account_number = "1234567890" # In a real scenario, this would be g

# Public method to get account balance


def get_balance(self):
return f"Account holder: {self.account_holder}, Balance: {self._balance}"

# Protected method to update balance (for internal use)


def _update_balance(self, amount):
if self._balance + amount >= 0:
self._balance += amount
return True
else:
return False

# Public method to deposit money


def deposit(self, amount):
if amount > 0:

Python 37
self._update_balance(amount)
return f"Deposited {amount}. New balance is {self._balance}."
else:
return "Deposit amount must be positive."

# Public method to withdraw money


def withdraw(self, amount):
if amount > 0 and self._update_balance(-amount):
return f"Withdrew {amount}. New balance is {self._balance}."
else:
return "Insufficient funds or invalid withdrawal amount."

# Private method to get account number (for internal use)


def __get_account_number(self):
return self.__account_number

# Public method to access private account number securely


def get_account_info(self):
return f"Account holder: {self.account_holder}, Account number: {self.__get

# Creating an object of the class


account = BankAccount("John Doe", 1000)

# Accessing public attributes and methods


print(account.account_holder) # Output: John Doe
print(account.get_balance()) # Output: Account holder: John Doe, Balance:
print(account.deposit(500)) # Output: Deposited 500. New balance is 1500
print(account.withdraw(300)) # Output: Withdrew 300. New balance is 1200.

# Accessing protected attributes and methods (not recommended, but possible)


print(account._balance) # Output: 1200 (directly accessing protected
print(account._update_balance(-200)) # Output: True (directly calling protected m

# Attempting to access private attributes and methods directly (will raise an Attri
# print(account.__account_number) # This will raise an AttributeError
# print(account.__get_account_number()) # This will raise an AttributeError

# Accessing private attributes/methods via public methods


print(account.get_account_info()) # Output: Account holder: John Doe, Account

# Accessing private attribute using name mangling (not recommended, but possible)
print(account._BankAccount__account_number) # Output: 1234567890

1. Public Attributes and Methods:

is a public attribute, and get_balance , deposit ,


account_holder withdraw , and get_account_info are public
methods. These can be accessed from outside the class.

Python 38
Example: account.account_holder , account.get_balance() .

2. Protected Attributes and Methods:

_balanceis a protected attribute, and _update_balance is a protected method. By convention, these


should only be accessed within the class and subclasses. However, they can still be accessed
directly from outside, though it's not recommended.

Example: account._balance , account._update_balance(-200) .

3. Private Attributes and Methods:

__account_number is a private attribute, and __get_account_number is a private method. These are not
accessible directly from outside the class. Python uses name mangling to make them accessible
only within the class.

You can access them indirectly using a public method or through name mangling.

Example: account.get_account_info() or account._BankAccount__account_number .

We are creating our class for handling exception handling.

class UnderageError(Exception):
def __init__(self, age, minimum_age=18):
self.age = age
self.minimum_age = minimum_age
super().__init__(f"Age {age} is below the minimum allowed age of {minimum_a

def verify_age(age):
try:
if age < 18:
# Raise the custom exception if age is below 18
raise UnderageError(age)
else:
return "Access granted!"
except UnderageError as e:
return str(e) # Return the error message as a string

# Testing the modified function


print(verify_age(15)) # Output: Age 15 is below the minimum allowed age of 18.
print(verify_age(20)) # Output: Access granted!

Custom Exception ( UnderageError ):

The UnderageError class remains the same.

Modified verify_age Function:

Inside the verify_age function, the try block checks the age.

If the age is less than 18, it raises the UnderageError .

The except block catches this specific exception and returns the error message as a string.

Python 39
Handling Inside the Function:

Now, the verify_age function handles the exception internally, so it returns a message whether the
age is sufficient or not.

Logging
Logging is a way to track events that happen when some software runs. The logging module in Python
allows you to track the flow of a program and diagnose issues by logging information to the console, a
file, or other destinations.

Logging has different severity levels (DEBUG, INFO, WARNING, ERROR, and CRITICAL).

import logging

formatter = logging.Formatter('%(asctime)s-%(levelname)s-%(name)s-%(message)s')
logging.basicConfig(filename='logFile.log', level=logging.DEBUG, filemode='a', form

logging.debug("This is a debug message")


logging.info("This is an info message")
logging.warning("This is a warning message")
logging.error("This is an error message")
logging.critical("This is an critical message")

When to use which severity:-

1. DEBUG - Detailed information typically of interest only when diagnosing problems.

2. INFO - Confirmation that things are working as expected.

3. WARNING - An indication that something unexpected happened or indicative of some problem in the near
future (eg: disk space is low). The software is still working as expected.

4. ERROR - Due to a more serious problem, the software has not been able to perform some function.

5. CRITICAL - A very serious error, indication that the program itself may be unable to continue running.

Creating a custom logger and saving it to a log file.

import logging

# By default when we run this file then the level will display as root, but when th
logger = logging.getLogger(__name__)

# Defining the level of the logger


logger.setLevel(logging.DEBUG)

# Setting the format of the custom logger created


formatter = logging.Formatter('%(asctime)s-%(levelname)s-%(name)s-%(message)s')

Python 40
# Creating a filehandler to store all the logs into a log file
fh = logging.FileHandler('SaveLog.log')

# The content that needs to be in the SaveLog is being defined below


fh.setFormatter(formatter)

# Add the file handler to the logger (basically linking the custom logger and the c
logger.addHandler(fh)

Let’s create a simple password checker example with logging. The password checker will check if the
password meets certain criteria (length, presence of numbers, etc.) and log different levels of
messages depending on the result.

import logging

# Step 1: Create a custom logger


logger = logging.getLogger(__name__)

# Step 2: Set the log level (DEBUG means it will log everything from DEBUG and abov
logger.setLevel(logging.DEBUG)

# Step 3: Create a formatter that specifies how the log messages should look
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s

# Step 4: Create a file handler to store the logs in a file


file_handler = logging.FileHandler('password_checker.log')
file_handler.setFormatter(formatter)

# Step 5: Add the file handler to the logger


logger.addHandler(file_handler)

# Step 6: Password checker function


def check_password(password):
logger.debug("Checking password...")

if len(password) < 8:
logger.warning("Password is too short!")
return "Password too short!"

if not any(char.isdigit() for char in password):


logger.warning("Password does not contain any numbers!")
return "Password must contain at least one number!"

if not any(char.isupper() for char in password):


logger.warning("Password does not contain any uppercase letters!")

Python 41
return "Password must contain at least one uppercase letter!"

logger.info("Password is valid.")
return "Password is valid!"

# Example usage of the password checker


passwords_to_test = ["short", "alllowercase123", "NoNumbersHere", "ValidPass123"]

for pwd in passwords_to_test:


result = check_password(pwd)
print(result)

Multithreading & Multiprocessing


To understand multithreading and multiprocessing we first need to get familiar with some terms which
are Program, Process and Threads.

Program - A sequence of instructions which are written in a programming language.

Process - A process is an instance of a program that is being executed. A process runs independently
and is not interrupted by other processes.

Thread - A thread in computing is the smallest unit of execution within a process. Threads allow a
program to perform multiple tasks concurrently (at the same time) within the same program. Each
thread runs independently but shares the same memory space and resources of the process it belongs
to.

Multithreading
We usually use multi-threading when we perform I/O operations. Here the I/O operations can be file
operations, network requests etc.

import threading
import time

def print_numbers():
for i in range(1, 6):
print(f"Number: {i}")
time.sleep(2) # Simulate a time-consuming task

def print_letters():
for letter in ['A', 'B', 'C', 'D', 'E']:
print(f"Letter: {letter}")
time.sleep(2) # Simulate a time-consuming task

# Creating two threads


thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_letters)

Python 42
# Start the threads
thread1.start()
thread2.start()

# Wait for both threads to finish


thread1.join()
thread2.join()

print("Both threads finished execution!")

threading.Thread() : Creates a new thread where target specifies the function that the thread will run.
Here, print_numbers and print_letters are assigned to separate threads.

start() : Starts the thread and allows it to run concurrently with other threads.

join() : Ensures that the main program waits for the threads to finish their execution before moving on

to the next line ( print("Both threads finished execution!") ).

time.sleep() : Simulates a time-consuming task by causing the thread to pause for 1 second after each
operation.

Thread Pool Executor

The main difference between a thread pool executor and the code above is that, in the above code we
are manually defining the number of thread and for that we are defining individual variables for that.
But in the case of thread pool executor, we are just defining the threads to be used in the program and
that is it. We need not define the variables for each and every thread.

from concurrent.futures import ThreadPoolExecutor, as_completed


import time

def print_square(number):
"""Function to print the square of a number."""
print(f"Calculating square of {number}")
time.sleep(1) # Simulate a time-consuming task
result = number * number
print(f"Square of {number} is {result}")
return result

def print_cube(number):
"""Function to print the cube of a number."""
print(f"Calculating cube of {number}")
time.sleep(1) # Simulate a time-consuming task
result = number * number * number
print(f"Cube of {number} is {result}")
return result

def main():
numbers = [1, 2, 3, 4, 5]

Python 43
# Create a ThreadPoolExecutor with a pool of 3 threads
with ThreadPoolExecutor(max_workers=3) as executor:
# Submit tasks to the executor
futures = []
for number in numbers:
futures.append(executor.submit(print_square, number))
futures.append(executor.submit(print_cube, number))

# Retrieve results as they complete


for future in as_completed(futures):
result = future.result()
print(f"Result: {result}")

if __name__ == "__main__":
main()

Import the Required Classes:

ThreadPoolExecutor to manage the pool of threads.

as_completed to handle results as tasks complete.

Define Functions:

print_square and print_cube are functions that simulate time-consuming tasks.

Create a ThreadPoolExecutor:

with ThreadPoolExecutor(max_workers=3) as executor creates a pool of up to 3 threads.

Submit Tasks:

submits tasks to the thread pool. Each task runs the specified function
executor.submit(func, *args)

with the provided arguments.

Retrieve Results:

as_completed(futures) yields futures as they complete, allowing you to process results or handle
exceptions.

Multiprocessing
We usually use multi-processing for CPU bound tasks, like when we need more computation power,
like a complex calculation.

While implementing multiprocessing we must use if __name__ == "__main__" , if we do not use this, we will
be prompted with an error.

import multiprocessing
import time

def print_numbers():

Python 44
for i in range(1, 6):
print(f"Number: {i}")
time.sleep(1) # Simulate a time-consuming task

def print_letters():
for letter in ['A', 'B', 'C', 'D', 'E']:
print(f"Letter: {letter}")
time.sleep(1) # Simulate a time-consuming task

def main():
# Create processes
process1 = multiprocessing.Process(target=print_numbers)
process2 = multiprocessing.Process(target=print_letters)

# Start processes
process1.start()
process2.start()

# Wait for both processes to complete


process1.join()
process2.join()

print("Both processes finished execution!")

if __name__ == "__main__":
main()

Importing the Module: import multiprocessing provides access to the multiprocessing functionality.

Defining Functions: print_numbers and print_letters are functions that will be executed by different
processes.

Creating Processes:

multiprocessing.Process(target=print_numbers) creates a new process that will execute the print_numbers

function.

Similarly, multiprocessing.Process(target=print_letters) creates a process for print_letters .

Starting Processes:

process1.start() and process2.start() start the execution of the processes.

Joining Processes:

process1.join() and process2.join() ensure that the main program waits for these processes to

complete before continuing.

Process Pool Executor

The main difference between a process pool executor and the code above is that, in the above code
we are manually defining the number of thread and for that we are defining individual variables for

Python 45
that. But in the case of process pool executor, we are just defining the process to be used in the
program and that is it. We need not define the variables for each and every thread.

from concurrent.futures import ThreadPoolExecutor, as_completed


import time

def print_square(number):
"""Function to print the square of a number."""
print(f"Calculating square of {number}")
time.sleep(1) # Simulate a time-consuming task
result = number * number
print(f"Square of {number} is {result}")
return result

def print_cube(number):
"""Function to print the cube of a number."""
print(f"Calculating cube of {number}")
time.sleep(1) # Simulate a time-consuming task
result = number * number * number
print(f"Cube of {number} is {result}")
return result

def main():
numbers = [1, 2, 3, 4, 5]

# Create a ThreadPoolExecutor with a pool of 3 threads


with ThreadPoolExecutor(max_workers=3) as executor:
# Submit tasks to the executor
futures = []
for number in numbers:
futures.append(executor.submit(print_square, number))
futures.append(executor.submit(print_cube, number))

# Retrieve results as they complete


for future in as_completed(futures):
result = future.result()
print(f"Result: {result}")

if __name__ == "__main__":
main()

Import the Required Classes:

ThreadPoolExecutor to manage the pool of threads.

as_completed to handle results as tasks complete.

Define Functions:

print_square and print_cube are functions that simulate time-consuming tasks.

Python 46
Create a ThreadPoolExecutor:

with ThreadPoolExecutor(max_workers=3) as executor creates a pool of up to 3 threads.

Submit Tasks:

executor.submit(func, *args) submits tasks to the thread pool. Each task runs the specified function
with the provided arguments.

Retrieve Results:

as_completed(futures) yields futures as they complete, allowing you to process results or handle
exceptions.

Python 47

You might also like