0% found this document useful (0 votes)
11 views

Python

Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
11 views

Python

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

Linear Data Structure

 A Linear Data Structure is a type of data structure in which elements are arranged in a
sequential or linear order, where each element is connected to its previous and next
element.
 In a linear data structure, all elements are stored in a single level, and traversal of data can
occur in a single run, either forward or backward.
Example :
Array: Representing a 2D Matrix for Addition
Linked List: Managing Dynamic Data
Stack: Implementing Browser Backtracking
Queue: Simulating a Print Job Queue
*********************************************************************
Applications of Data Structures:
1. Artificial Intelligence: Representing and searching decision trees, graphs, and state
spaces.
2. Operating Systems: Process scheduling, memory management, and file systems.
3. Networking: Routing algorithms, data packet organization, and bandwidth allocation.
4. Game Development: Pathfinding (A* algorithm), scene graphs, and event systems.
5. Machine Learning: Storing and processing datasets efficiently.
*************************************************************************
Comprehensions
1. List Comprehensions
 List comprehensions provide a concise way to create lists in Python.
 They use a single line of code with an expression and an optional condition to generate a
list based on an iterable.
Syntax:
[expression for item in iterable if condition]
Example:
print("List Comprehension:")
squares = [x**2 for x in range(10)]
print(f"Squares of numbers from 0 to 9: {squares}\n")
2. Dictionary Comprehensions
Dictionary comprehensions allow you to create dictionaries in a concise way by specifying keys
and values derived from an iterable.

Syntax:
{key_expression: value_expression for item in iterable if condition}
Example:
print("Dictionary Comprehension:")
number_map = {x: x**2 for x in range(5)}
print(f"Mapping of numbers to their squares (0 to 4): {number_map}\n")

3. Set Comprehensions
 Set comprehensions enable the creation of sets in a concise manner.
 Since sets only contain unique elements, duplicate values are automatically removed.
Syntax:
{expression for item in iterable if condition}
Example:
print("Set Comprehension:")
squares = {x**2 for x in [1, 2, 2, 3, 4, 4, 5]}
print(f"Unique squares from the list [1, 2, 2, 3, 4, 4, 5]: {unique_squares}\n")

4. Generator Comprehensions
 Generator comprehensions create a generator object instead of storing elements in
memory.
 They are memory-efficient and generate items lazily, i.e., only when requested.

Syntax:
(expression for item in iterable if condition)
Example:
print("Generator Comprehension:")
square_gen = (x**2 for x in range(5))
print("Squares of numbers from 0 to 4 generated one by one:")
for square in square_gen:
print(square)

 List Comprehensions: Creates a list where each element is the square of numbers from 0
to 9.
 Dictionary Comprehensions: Creates a dictionary where each key is a number, and the
value is its square for numbers from 0 to 4.
 Set Comprehensions: Creates a set of unique squares from the given list.
 Generator Comprehensions: Generates squares of numbers one by one in memory-
efficient manner.
**************************************************************************
List Comprehension Works in Python
 List comprehensions provide a concise way to create lists in Python.
 They use a single line of code with an expression and an optional condition to generate a
list based on an iterable.
List comprehension provides a concise way to create lists. It consists of:
1. Expression: An operation or value to include in the new list.
2. Iterable: A sequence or collection to iterate over.
3. Optional Condition: A filter to decide which elements to include.

Syntax:
[expression for item in iterable if condition]
Example:
squares = []
for x in range(10):
squares.append(x**2)

squares_comp = [x**2 for x in range(10)]


print("Traditional:", squares)
print("Comprehension:", squares_comp)

Output:
Traditional: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Comprehension: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
***************************************************************************
Linked lists
Linked lists are considered linear data structures. Despite their flexible memory allocation and
pointer-based connections, they maintain a sequential order of elements, which is a key
characteristic of linear data structures.
Why Linked Lists are Linear:
1. Sequential Access: Elements in a linked list are accessed sequentially, one after the other,
from the head node to the tail node.
2. Single Path: At any point in a linked list, there is only one logical next element to traverse,
making the structure linear in nature.
************************************************************************
Generators in Python:
 Generators are a special type of iterable in Python that allows you to produce values lazily,
meaning values are generated one at a time and only when requested.
 This is achieved using the yield keyword, which pauses the generator's state and can resume
from where it left off, making them more memory-efficient than lists.
Key Features of Generators:
1. Lazy Evaluation
2. State Retention
3. Iterables:
**************************************************************************
Inorder Traversal:
To solve the problem of finding all possible binary trees for a given inorder traversal and
printing their preorder traversals, we can use recursion. Each element in the inorder traversal
can serve as the root of the tree, and the elements to the left of the root in the array form the
left subtree, while the elements to the right form the right subtree.

def get_trees(inorder):
if not inorder:
return [None]
trees = []
for i in range(len(inorder)):
left = get_trees(inorder[:i])
right = get_trees(inorder[i+1:])
for l in left:
for r in right:
trees.append((inorder[i], l, r))
return trees

def preorder(tree):
if not tree:
return []
return [tree[0]] + preorder(tree[1]) + preorder(tree[2])

# Input and Output


inorder = [3, 2]
print("Preorder Traversals:")
for tree in get_trees(inorder):
print(preorder(tree))
*************************************************************************
Inheritance hierarchy based upon a Polygon class:
class Triangle:
def __init__(self, a, b, c, h):
self.a, self.b, self.c, self.h = a, b, c, h
def area(self): return 0.5 * self.a * self.h
def perimeter(self): return self.a + self.b + self.c

class Quadrilateral:
def __init__(self, l, w):
self.l, self.w = l, w
def area(self): return self.l * self.w
def perimeter(self): return 2 * (self.l + self.w)

class Pentagon:
def __init__(self, s):
self.s = s
def area(self): return self.s ** 2 * 1.72
def perimeter(self): return 5 * self.s

shape = input("Enter shape (Triangle/Quadrilateral/Pentagon): ").lower()

if shape == "triangle":
a, b, c, h = map(float, input("Enter sides and height: ").split())
poly = Triangle(a, b, c, h)
elif shape == "quadrilateral":
l, w = map(float, input("Enter length and width: ").split())
poly = Quadrilateral(l, w)
elif shape == "pentagon":
s = float(input("Enter side length: "))
poly = Pentagon(s)
else:
print("Invalid shape")
exit()

print(f"Area: {poly.area()}, Perimeter: {poly.perimeter()}")


****************************************************************************
Abstract base class called “Animal” using the ABC (Abstract Base Class) module
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def move(self):
pass

class Human(Animal):
def move(self):
print("Humans walk on two legs.")

class Snake(Animal):
def move(self):
print("Snakes slither on the ground.")

class Dog(Animal):
def move(self):
print("Dogs run on four legs.")

class Lion(Animal):
def move(self):
print("Lions run swiftly on four legs.")

for animal in [Human(), Snake(), Dog(), Lion()]:


animal.move()
***********************************************************************
To generate the combinations of n distinct objects taken from the elements of a given list.
Example: Original list: [1, 2, 3, 4, 5, 6, 7, 8, 9] Combinations of 2 distinct objects: [1, 2]
[1, 3] [1, 4] [1, 5] .... [7, 8] [7, 9] [8, 9].

Program
from itertools import combinations
def generate_combinations(input_list, n):
# Generate all combinations of n distinct objects
result = list(combinations(input_list, n))
return result

# Example usage
original_list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
n=2
print(f"Original list: {original_list}")
print(f"Combinations of {n} distinct objects:")

combinations_result = generate_combinations(original_list, n)
for combo in combinations_result:
print(list(combo))

Output :
[1, 2]
[1, 3]
[1, 4]
...
[7, 8]
[7, 9]
[8, 9]
***************************************************************************
Python Lambdas Useful
 Python lambda functions are useful for writing small, anonymous functions inline without
the need for a full def statement.
 They are especially helpful in scenarios where you need a quick, one-time function, such
as with map(), filter(), and reduce().

Key Benefits of Lambdas:


1. Concise Syntax: Allows you to define small functions in a single line.
2. Anonymous: No need to assign a name to the function unless necessary.
3. Improved Readability: Ideal for short, simple operations.

Program to Print Even Numbers (1 to 100) Using lambda:


# Using lambda and filter to find even numbers
even_numbers = list(filter(lambda x: x % 2 == 0, range(1, 101)))

# Printing the even numbers


print("Even numbers from 1 to 100:")
print(even_numbers)

Output:
Even numbers from 1 to 100:
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, ..., 98, 100]

Advantages of Lambda Functions


 Concise and Readable:
 No Need for Naming
 Efficient for Short Use Cases
 Integration with Functional Programming

Disadvantages of Lambda Functions


 Limited Functionality
 Reduced Readability for Complex Logic
 No Documentation or Annotations
 Debugging Challenges

Applications of Lambda Functions


 Data Transformation with map
 Filtering Data with filter
 Sorting with Custom Keys:
 Reducing Data with reduce
 Event Handling
********************************************************************
list as a Dictionary key
Lists cannot be used as dictionary keys in Python because they are mutable, which means their
contents can change after creation. In Python, dictionary keys must be hashable, and mutability
makes a list non-hashable.
Reason:
 Dictionary keys must be immutable and hashable.
 Lists are mutable (their content can change after creation), which makes them unhashable.
Therefore, attempting to use a list as a key in a dictionary will raise a TypeError.
***************************************************************************
Convert a List to a String
Using join() for Lists of Strings
The join() method concatenates the elements of a list into a single string. This method works
when all elements in the list are strings.
# List of strings
words = ["Python", "is", "fun"]
# Convert list to a single string
result = " ".join(words)
print(result)
******************************************************************************
mixin
A mixin is a special kind of class in Python that is designed to provide additional functionality to
other classes through inheritance. A mixin typically does not stand alone as a fully functional
class and is not meant to be instantiated directly. Instead, it is used to "mix in" additional
behavior to other classes when combined with multiple inheritance.
Key Characteristics of a Mixin:
1. Focus on Adding Functionality:
o A mixin usually provides a set of methods or attributes that can be reused across
multiple classes.
2. Not Intended for Direct Instantiation:
o A mixin is not a standalone class and does not usually implement all the
functionality needed to be instantiated.
3. Used with Multiple Inheritance:
o Mixins work well in Python due to its support for multiple inheritance, allowing
classes to combine behaviors from multiple mixins.
4. Separation of Concerns:
o Mixins allow functionality to be modularized into reusable pieces, promoting
better code organization and reusability.

Why Are Mixins Useful?


 Code Reuse: They help avoid code duplication by encapsulating reusable behavior in a
mixin.
 Modularity: They allow functionalities to be broken down into separate, self-contained
classes.
 Avoiding Deep Inheritance Chains: Mixins reduce the need for complex inheritance
hierarchies by enabling horizontal composition.
Example:
class LoggingMixin:
def log(self, message):
print(f"[LOG]: {message}")
class FileHandler:
def read(self, filepath):
with open(filepath, 'r') as file:
return file.read()

class FileHandlerWithLogging(FileHandler, LoggingMixin):


def read(self, filepath):
self.log(f"Reading file: {filepath}")
return super().read(filepath)

handler = FileHandlerWithLogging()
handler.log("Starting the file operation")
print(handler.read("example.txt"))
*******************************************************************
factorial in Python
def factorial(n):
return 1 if n == 0 else n * factorial(n - 1)
# Test the function
num = 5
print(f"Factorial of {num} is {factorial(num)}")

Output:
Factorial of 5 is 120
***********************************************************************
Convert your List to a Set in Python
A set is an unordered collection of unique elements, so converting a list to a set will
automatically remove any duplicate elements.
list1 = [1, 2, 3]
list2 = [3, 4, 5]
set1 = set(list1)
set2 = set(list2)
print(set1 | set2) # Union: {1, 2, 3, 4, 5}
print(set1 & set2) # Intersection: {3}
print(set1 - set2) # Difference: {1, 2}
*************************************************************
Concept of inheritance and method overriding:
# Base class
class Animal:
def __init__(self, name):
self.name = name

def speak(self):
return f"{self.name} makes a sound."

# Derived class 1
class Dog(Animal):
def speak(self):
return f"{self.name} barks."

# Derived class 2
class Cat(Animal):
def speak(self):
return f"{self.name} meows."

# Main program
if __name__ == "__main__":
# Creating instances of the base and derived classes
generic_animal = Animal("Generic Animal")
dog = Dog("Rover")
cat = Cat("Whiskers")
# Demonstrating method overriding
print(generic_animal.speak()) # Calls the speak method from the Animal class
print(dog.speak()) # Calls the overridden speak method from the Dog class
print(cat.speak()) # Calls the overridden speak method from the Cat class

Output:
Generic Animal makes a sound.
Rover barks.
Whiskers meows.
********************************************************************8
append() and extend() methods
append() Method
 Purpose: Adds a single element to the end of the list.
 How it Works: The argument passed to append() is added as a single item, meaning if you
append a list, the entire list will be added as a single element.
extend() Method
 Purpose: Adds each element of an iterable (e.g., list, tuple, string) to the list individually.
 How it Works: The elements of the iterable are added to the list as individual items, not
as a single object.
Key Differences:

Method Action Example Output

append() Adds a single element (as an object) my_list.append([4, 5]) [1, 2, 3, [4, 5]]

extend() Adds elements of an iterable individually my_list.extend([4, 5]) [1, 2, 3, 4, 5]

******************************************************************************
call static method from within class:
It is possible to call a static method from within the same class without qualifying its name
in Python. Static methods belong to the class rather than any specific instance, so they can be called
using the class name, but they can also be invoked directly without qualification from within the
class scope.
Key Points:
1. Direct Call: When calling a static method within the same class, you can call it directly
by its name.
2. Using @staticmethod: Static methods are decorated with @staticmethod, which allows
them to be accessed without needing an instance or self.
3. No Access to Instance or Class (self or cls): Static methods do not take self or cls as their
first parameter.
class Example:
@staticmethod
def static_method():
print("Static method called")

def instance_method(self):
static_method()

example = Example()
example.instance_method()

Why It Works:
 Python looks up the method name in the local class scope, so the static method is found
and executed.
 The static method doesn’t depend on self or cls, so it can be invoked without needing an
instance or the class name.
**************************************************************************
Add an object to the global namespace: globals() or dir()
It is possible to add an object to the global namespace in Python using the globals()
function. The globals() function returns a dictionary representing the current global symbol
table, which can be modified to add or remove objects in the global namespace. However, dir()
cannot be used for this purpose, as it only lists the names in the current scope and is not modifiable.
Example: Adding an Object to the Global Namespace Using globals() .
# Define a class
class MyClass:
def __init__(self, name):
self.name = name

def greet(self):
return f"Hello, {self.name}!"

# Adding an object to the global namespace


obj = MyClass("Alice")
globals()['my_global_object'] = obj

print(my_global_object.greet()) # Access using the name added to globals()

Why dir() Cannot Be Used:


The dir() function only returns a list of names in the current scope and does not allow
modification. It is a read-only utility for inspecting the available names.
*****************************************************************
Use the class as a namespace
Using a class as a namespace in Python can be a valid and useful approach in some situations, but
it comes with trade-offs that need to be carefully considered.
Grouping Related Constants or Functions:
 A class can be used to group related constants, functions, or values to provide logical
organization.
class Config:
DATABASE_URL = "localhost:5432"
TIMEOUT = 30
DEBUG = True
print(Config.DATABASE_URL) # Access constants
Readability and Organization:
 Using a class as a namespace makes it clear that certain values or functions belong
together, improving code readability.
Avoiding Global Namespace Pollution:
 Classes prevent names from cluttering the global namespace, helping to organize large
codebases.
No Need for Instantiation:
 Such classes often do not need to be instantiated, and their members are accessed directly
using the class name.
When is it Not a Good Idea
 Misuse of Object-Oriented Principles:
 Potential Misuse by Adding State or Behavior:
 Readability for Non-Static Members
Example: Class as a Namespace
class MathConstants:
PI = 3.14159
E = 2.71828
print(MathConstants.PI)
**********************************************************************
Interfaces in Python
Interfaces are implemented using abstract base classes (ABCs). The abc module provides
the tools to define abstract base classes and enforce that derived classes implement specific
methods. Abstract methods are defined using the @abstractmethod decorator.
Steps to Implement Interfaces in Python
1. Define an Abstract Base Class (Interface):
2. Implement the Interface in a Subclass:
3. Optional Multiple Inheritance:

Defining an Interface
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""Calculate the area of the shape."""
pass

@abstractmethod
def perimeter(self):
"""Calculate the perimeter of the shape."""
pass

Implementing the Interface in Subclasses


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

def area(self):
return self.length * self.width

def perimeter(self):
return 2 * (self.length + self.width)

class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
*****************************************************************
Constructor not overridden
In Object-Oriented Programming (OOP), constructors are not overridden because they are
not inherited in the same way as regular methods. A constructor is specific to the class in which it
is defined, and each class has its own constructor. When an object of a subclass is created, the
constructor of the subclass is executed, and if it explicitly calls the constructor of the superclass, it
does so using the super() keyword.
Key Points:
1. Not Overridden: Constructors are not overridden because they are designed to initialize
specific properties for their class.
2. Superclass Constructor: If the subclass does not define its constructor, the superclass
constructor is called by default (if it has a no-argument constructor).
3. Explicit Call: The super() keyword is used in the subclass constructor to call the
superclass constructor explicitly.

class Parent:
def __init__(self):
print("Constructor of Parent class")

class Child(Parent):
def __init__(self):
print("Constructor of Child class")
super().__init__() # Explicitly calling Parent class constructor

# Create an object of the Child class


child_obj = Child()
*********************************************************************
Namespace package in Python
 A namespace package allows you to split a package across multiple directories.
 Python automatically combines them when you import modules from these directories.

To create a namespace package in Python, you can follow the PEP 420 method, which does not
require an __init__.py file in the package directories.
Steps:
1. Create the directory structure without an __init__.py file in the top-level package.
2. Distribute modules across multiple directories that share the same namespace.

Step-by-Step Example:
Step 1: Create the Folder Structure
Make two directories for the same package:
project/
├── part1/
│ └── module1.py
└── part2/
└── module2.py
 part1 and part2 are sub-packages of the same namespace.
 Note: Do not add an __init__.py file in part1 or part2. This tells Python they are part of a
namespace package.
Step 2: Add Python Code in Modules
part1/module1.py:
def greet():
print("Hello from module1 in part1!")

part2/module2.py:
def greet():
print("Hello from module2 in part2!")
Step 3: Using the Namespace Package
You can now import and use the modules from both parts:
import part1.module1 # Import module from part1
import part2.module2 # Import module from part2

# Call the functions


part1.module1.greet() # Output: Hello from module1 in part1!
part2.module2.greet()

Benefits of Namespace Packages:


 Modularity: You can split your package across multiple directories or even repositories.
 Collaboration: Different teams can work on different parts of the package.
 Scalability: Easily extend packages without affecting the existing structure.
***********************************************************************
Sorting algorithm
 The fastest for all use cases, as the performance of a sorting algorithm depends on the
nature of the input data, the size of the dataset, and specific requirements like memory
usage or stability.
 Quicksort is often considered one of the fastest sorting algorithms in practice for large
datasets due to its efficiency in most real-world scenarios.

Why Quicksort is Often Fastest:


1. Divide-and-Conquer Approach: Quicksort partitions the data into smaller subsets,
making it highly efficient for large datasets.
2. Average-Case Time Complexity:
o Quicksort has an average-case time complexity of O(n log n), which is very
efficient for most input scenarios.
o It only degrades to O(n²) in the worst case (e.g., if the pivot selection is poor and
leads to uneven partitions).
3. Cache Efficiency:
o Quicksort's in-place sorting minimizes memory usage and leverages spatial
locality, making it faster on modern hardware.
4. Flexibility:
o It can be easily optimized using techniques like randomized pivot selection or the
Median-of-Three rule to mitigate worst-case performance.
5. Smaller Constant Factors

When Quicksort Might Not Be Fastest:


1. Stable Sorting Required
2. Already Sorted or Nearly Sorted Input
3. Memory Constraints:

Other Fast Sorting Algorithms in Context:


1. Timsort:
o Used in Python and Java standard libraries.
o Combines Merge Sort and Insertion Sort for excellent performance on real-world
data.
2. Merge Sort:
o Preferred for linked lists and external sorting.
o Stable and guarantees O(n log n) time.
3. Radix Sort:
o Outperforms comparison-based algorithms like Quicksort for data with a fixed
range (e.g., integers).
****************************************************************************
When to Use Linked Lists and Array :
Linked lists and arrays are fundamental data structures, each with strengths and
weaknesses. Choosing between them depends on the specific requirements of a scenario, such as
memory usage, access time, and the frequency of insertion or deletion operations.

When to Use Linked Lists


A linked list is a dynamic data structure where elements (nodes) are linked using pointers. It is
suitable for scenarios where:
1. Dynamic Size is Needed:
o The number of elements is unknown or changes frequently.
o Example: Implementation of stacks and queues with a variable number of
elements.
2. Frequent Insertions and Deletions:
o Adding or removing elements in the middle or at the beginning is efficient (O(1)
if the pointer to the node is known).
o Example: Maintaining a sorted list where frequent additions or deletions occur.
3. Memory Efficiency with Sparse Data:
o Efficient use of memory when the dataset is sparse and not densely packed.
o Example: Representation of adjacency lists in graph implementations.
4. Avoiding Reallocation Overheads:
o No need for resizing, as nodes are dynamically allocated.
o Example: Handling datasets that grow unpredictably, like undo functionality in
text editors.
5. Non-Contiguous Memory Allocation:
o Useful when memory is fragmented, and contiguous allocation (as needed by
arrays) is challenging.
o Example: Managing large datasets on memory-constrained systems.
When to Use Arrays
An array is a static or dynamic data structure that stores elements in contiguous memory
locations. It is suitable for scenarios where:
1. Fast Random Access is Needed:
o Provides O(1) time complexity for accessing any element via its index.
o Example: Lookup tables, storing and accessing images or sound samples.
2. Fixed or Predictable Size:
o The size of the dataset is known beforehand, or resizing is manageable.
o Example: Storing matrix data for mathematical computations.
3. Frequent Sequential Access:
o Accessing elements sequentially is efficient due to better cache locality.
o Example: Iterating through a list of items like logs or records.
4. Low Overhead and Simple Implementation:
o No need for pointer management or additional memory for node structures.
o Example: Storing simple datasets like scores or coordinates.
5. Sorting and Searching:
o Arrays work well with algorithms like Binary Search or QuickSort because of
direct access and contiguous storage.
o Example: Maintaining a sorted dataset for quick lookups.
Choosing Between Linked Lists and Arrays
1. Use linked lists if:
o Frequent insertions/deletions are needed.
o The size of the data structure changes dynamically.
o Memory allocation is fragmented or unpredictable.
2. Use arrays if:
o Fast access to elements is required.
o The size of the data is fixed or changes rarely.
o Sequential processing or operations like sorting and searching are common.
******************************************************************************
Linked Lists better than arrays
Linked lists are better than arrays in specific scenarios due to their dynamic nature and flexibility.
Below are the key ways in which linked lists outperform arrays, along with reasoning:
1. Dynamic Size
 Linked List: The size of a linked list can grow or shrink dynamically without the need
for reallocation or resizing.
 Array: An array has a fixed size (or requires costly resizing operations for dynamic
arrays like ArrayList or Vector in some languages).
Reasoning: Linked lists allocate memory as needed for each element, making them ideal for
scenarios where the size of the data structure is unpredictable.

2. Efficient Insertions and Deletions


 Linked List: Insertion and deletion at the head, tail, or anywhere in the list (given a
reference to the node) is efficient, typically O(1).
 Array: Insertion or deletion requires shifting elements, resulting in O(n) complexity in
the worst case.
Reasoning: Linked lists use pointers to link elements, so adding or removing nodes doesn't
affect the overall structure, unlike arrays that need to maintain contiguity.

3. No Need for Reallocation


 Linked List: No need to reallocate or copy elements when the structure grows.
 Array: Dynamic arrays require resizing, which involves creating a new array and
copying all elements.
Reasoning: Resizing arrays is computationally expensive and can lead to significant overhead,
particularly for large datasets.

4. No Wastage of Memory
 Linked List: Allocates memory for only the required number of elements, avoiding
wastage.
 Array: May allocate extra memory to accommodate future growth in dynamic arrays or
leave unused space in static arrays.
Reasoning: Arrays often allocate excess memory to handle potential resizing, which can lead to
unused memory overhead.

Advantage Linked List Why It’s Better

Flexible, no reallocation Efficient for unpredictable growth or


Dynamic resizing
needed shrinkage.

Insertion/Deletion O(1) with node reference No need to shift elements as in arrays.

Works even in fragmented memory


Memory utilization Uses fragmented memory
environments.

Avoids resizing No reallocation or Saves time and reduces computational


overhead copying overhead.
Advantage Linked List Why It’s Better

Supports cyclic and Can be adapted for specific applications


Custom structures
doubly-linked (e.g., queues, graphs).

***************************************************************************

Convert a Queue into the Stack


def queue_to_stack(queue):
# Initialize an empty stack
stack = []

# Transfer elements from the queue to the stack


while queue:
stack.append(queue.pop(0)) # Remove from the front of the queue and add to the stack

return stack

# Example usage
if __name__ == "__main__":
# Create a queue (using a list)
queue = [10, 20, 30, 40, 50]

print("Original Queue:", queue)

# Convert queue to stack


stack = queue_to_stack(queue)

print("Converted Stack:", stack)

Example Output
For an input queue [10, 20, 30, 40, 50]:
Original Queue: [10, 20, 30, 40, 50]
Converted Stack: [10, 20, 30, 40, 50]

Explanation
1. Queue Representation:
o The queue is represented as a list, where the front of the queue is the first element
(queue[0]).
2. Conversion:
o Using a while loop, elements are removed from the front of the queue (pop(0)) and
added to the stack (append()).
3. Result:
o The stack will contain the reversed order of the original queue.
******************************************************************************
Detect a loop in a linked list
Detecting a loop in a linked list is a common problem that can be solved using several methods.
The most efficient approach is the Floyd’s Cycle Detection Algorithm (also known as the
tortoise and hare algorithm).
def detect_loop(head):
slow, fast = head, head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False

Usage Example
class Node:
def __init__(self, data):
self.data = data
self.next = None

head = Node(1)
head.next = Node(2)
head.next.next = Node(3)
head.next.next.next = Node(4)
head.next.next.next.next = head.next # Creates a loop
# Detect loop
print("Loop detected!" if detect_loop(head) else "No loop detected!")
**************************************************************************
Advantages of Linked Lists over Arrays
1. Dynamic Size:
o A linked list can dynamically grow or shrink during execution, while the size of an
array is fixed once declared.
2. Efficient Insertions and Deletions:
o Insertions and deletions in a linked list are efficient as they involve updating
pointers, whereas in arrays, elements need to be shifted, which is time-consuming.
3. Memory Utilization:
o Linked lists use memory efficiently as memory is allocated on demand. Arrays may
lead to unused memory (if the array is under-utilized) or overflow (if the array is
fully utilized).
4. No Contiguous Memory Requirement:
o Linked lists do not require contiguous memory allocation, unlike arrays, making
them suitable for systems with fragmented memory.
5. Ease of Implementation for Certain Data Structures:
o Data structures such as stacks, queues, and hash chains are easier to implement with
linked lists due to their dynamic nature.
When to Use a Linked List?
 Frequent Insertions/Deletions:
 Unknown Data Size:
 Memory Fragmentation
 Complex Relationships
When to Use an Array?
1. Frequent Access by Index:
2. Static and Predictable Data Size:
3. Cache-Friendly Operations:
4. Simple Applications:
5. Sorting and Searching:
****************************************************************
Remove Duplicates From a List in Python
1. Using set()
The set data structure inherently removes duplicates because it doesn't allow duplicate values.
numbers = [1, 2, 2, 3, 4, 4, 5, 6, 6, 7]
unique_numbers = list(set(numbers))

print("Original List:", numbers)


print("List After Removing Duplicates:", unique_numbers)

Output:
Original List: [1, 2, 2, 3, 4, 4, 5, 6, 6, 7]
List After Removing Duplicates: [1, 2, 3, 4, 5, 6, 7]

Using a Loop
You can iterate through the list and add elements to a new list only if they are not already
present.
numbers = [1, 2, 2, 3, 4, 4, 5, 6, 6, 7]

unique_numbers = []
for num in numbers:
if num not in unique_numbers:
unique_numbers.append(num)
print("Original List:", numbers)
print("List After Removing Duplicates:", unique_numbers)
Output:
Original List: [1, 2, 2, 3, 4, 4, 5, 6, 6, 7]
List After Removing Duplicates: [1, 2, 3, 4, 5, 6, 7]
***********************************************************************
Super()
 In Python, super() is a built-in function that is used to call methods from a parent (or
superclass) from within a subclass.
 It is commonly used to invoke the constructor (__init__()) of the parent class to ensure
proper initialization of the inherited attributes and methods, especially when working with
inheritance.
 The main purpose of super() is to provide a way for a child class to call the methods or
attributes of its parent class without explicitly naming the parent class.
 This supports cleaner, more maintainable, and more flexible code, especially in complex
class hierarchies.
super().__init__()
 Purpose: The super().__init__() is used to call the constructor of the parent class in a way
that respects the method resolution order (MRO) in multiple inheritance scenarios. It
ensures that Python calls the appropriate method in the inheritance chain.
Explicit Superclass __init__()
 Purpose: Calling the superclass's __init__() directly (i.e., by explicitly naming the class)
bypasses the method resolution order (MRO) and directly calls the constructor of the
specified parent class.
When to Use Each Approach
 Use super().__init__() when you want to:
o Ensure that the method resolution order (MRO) is respected.
o Work with multiple inheritance, where you may have more than one parent class.
o Write code that is more maintainable and flexible.
 Use explicit Superclass.__init__(self) when:
o You have a simple inheritance structure (single inheritance) and want to directly
call the constructor of the parent class.
o You are certain about which class's constructor should be called and don't need to
rely on the MRO.
******************************************************************************
This () and super () both in a constructor:
 you can use both self() and super() in a constructor in Python.
 It's important to understand their respective purposes and how they interact when used in
the context of a constructor.
Understanding self() and super() in Constructors
 self() is used to refer to the current instance of the class. In a constructor (__init__), it is
used to assign or initialize the instance variables of that class.
 super() is used to call a method (usually the constructor) from the parent or superclass.
It allows the child class to access the parent class’s methods, including its constructor.
When to Use super() in a Constructor
 super() is typically used in a child class’s constructor (__init__) to call the parent class’s
constructor, especially if the parent class has its own initialization logic.
 This ensures that the initialization logic of the parent class is executed before or after the
initialization in the child class.
************************************************************************8
classes inherit object
 All classes (whether explicitly inherited or not) inherit from a special base class called
object.
 This is because, in Python, every class is an instance of the object class, which is the root
of the class hierarchy.
 This inheritance from object is a fundamental part of Python's object-oriented design.
Python Classes Inherit from object
 Unified Base Class
 Object-Oriented Behavior
 Access to Core Methods
 Consistency and Extensibility
New-Style Classes
 All classes in Python inherit from object, whether explicitly or implicitly.
 Inheritance from object provides a unified base class, giving all Python classes consistent
behaviors and features.
 The inheritance from object enables features like method resolution, polymorphism, and
special methods (__str__, __init__, etc.).
 In Python 3, all classes are new-style classes by default, which means they implicitly inherit
from object.
 In Python, classes inherit from object to ensure that they are part of a unified object-
oriented model, which allows the usage of common methods and behaviors across all
classes. This inheritance from object is fundamental to Python's design, enabling
consistency, flexibility, and extensibility in object-oriented programming.
*************************************************************************
Differences Between References and Pointers
 An abstract class in Python is a class that cannot be instantiated on its own. It serves as a
blueprint for other classes, defining methods that must be implemented by any subclass.
 The primary purpose of an abstract class is to define a common interface or a set of methods
that derived classes must implement
Key Concepts of Abstract Classes in Python:
1. Abstract Methods:
o An abstract class can have one or more abstract methods. An abstract method is a
method that is declared but contains no implementation in the abstract class.
o Subclasses of the abstract class are required to implement these abstract methods.
2. Cannot Instantiate Abstract Classes:
o You cannot create an instance of an abstract class directly. It can only be used as a
base class for other classes.
3. Abstract Base Class (ABC) Module:
o Python's abc module provides the functionality to define abstract classes and
methods.
o To create an abstract class, the class must inherit from the ABC (Abstract Base
Class) class provided by the abc module.
4. Use of the @abstractmethod Decorator:
o The @abstractmethod decorator is used to mark methods as abstract methods,
indicating that they must be implemented by any subclass.
****************************************************************************
Lists, Tuples, and Sets
Property List Tuple Set
Ordered, mutable Ordered, immutable Unordered, mutable collection
Definition
collection of elements. collection of elements. of unique elements.

Syntax

Property List Tuple Set


Syntax [ ] () {}
Example [1, 2, 3] (1, 2, 3) {1, 2, 3}

Key Characteristics

Property List Tuple Set


Ordered (maintains the Ordered (maintains the Unordered (elements are
Order
order of elements). order of elements). not stored in order).
Mutable (can add, remove, Immutable (elements Mutable (can add or
Mutability
or change elements). cannot be changed). remove elements).
Duplicates Allows duplicates. Allows duplicates. Does not allow duplicates.

***********************************************************************
Brute Force and Backtracking
Brute Force Approach
 It involves systematically generating and evaluating all possible solutions to find the
correct one.
 Key Features:
o Exhaustive Search.
o No Optimization.
o High Time Complexity
 Example:
o Solving the Traveling Salesman Problem by evaluating all permutations of
cities to find the shortest route.
Backtracking Approach
 A smarter way of exploring possible solutions by incrementally building candidates and
abandoning (or "backtracking") when a candidate fails to satisfy constraints.
 Key Features:
o Pruned Search Space
o Recursive Approach
o More Efficient
 Example:
o Solving the n-Queens Problem by placing queens one by one on the board and
backtracking if a conflict arises.
Comparison Table
Feature Brute Force Backtracking

Explores incrementally and prunes


Search Strategy Tries all possible solutions
paths

Efficient for problems with


Efficiency Inefficient, high time complexity
constraints

Constraint Ignores constraints until testing Applies constraints during


Handling solutions exploration

Implementation Iterative or recursive Primarily recursive

Application Small input sizes Large input sizes with constraints

******************************************************************************
Python program to convert a comma-separated list into a string:
# Input: A list with comma-separated values
comma_separated_list = ["apple", "banana", "cherry", "date"]

# Convert list to a single string


result_string = ", ".join(comma_separated_list)
# Output the result
print("Comma-separated string:", result_string)

Output
Comma-separated string: apple, banana, cherry, date
***********************************************************************
Python program to capitalize the first letter of a string:
# Input: A string from the user
input_string = input("Enter a string: ")

# Capitalize the first letter


capitalized_string = input_string.capitalize()

# Output the result


print("Capitalized string:", capitalized_string)

Example Output
Enter a string: hello world
Output:
Capitalized string: Hello world
**********************************************************************
Depth-First Search (DFS) and Breadth-First Search (BFS) algorithms
Depth-First Search (DFS):
 Traversal Order: DFS explores as deep as possible along each branch before
backtracking.
 Leaf Nodes: In a full tree, DFS can encounter a leaf node quickly if it dives deep into one
branch.
 Behavior:
o DFS does not examine nodes at the current level completely before moving
deeper.
o It may reach a leaf in the first branch it explores.
Breadth-First Search (BFS):
 Traversal Order: BFS explores all nodes at the current depth level before moving to the
next level.
 Leaf Nodes: In a full tree, BFS only encounters leaves after traversing all nodes at every
level above the leaves.
 Behavior:
o BFS takes longer to reach a leaf because it must process all nodes at intermediate
levels first.
Which Traverses a Leaf Soonest?
 DFS reaches a leaf node sooner because it directly explores one branch fully, reaching
the deepest nodes (leaves) quickly.
 BFS will only reach a leaf node after traversing all levels above it, making it slower to
encounter leaves in a full tree.
A
/ \
B C
/ \ /\
D E F G
DFS (Preorder):
 Traversal order: A → B → D (leaf encountered after 3 steps).
BFS:
 Traversal order: A → B → C → D (leaf encountered after 4 steps).
*********************************************************************
Use Python Lists , Use Tuples, Dictionaries or Sets

Comparison Table

Data
Order Duplicates Mutable Use Case
Structure

Sequential or variable-length collections where


List Ordered Allowed Mutable
items may change frequently.
Data
Order Duplicates Mutable Use Case
Structure

Fixed collections of items where immutability is


Tuple Ordered Allowed Immutable
required.

Keys: Key-value mappings for fast lookups and


Dictionary Unordered Mutable
Unique organization of data.

Not Collections requiring uniqueness or set operations


Set Unordered Mutable
Allowed (union, intersection).

*************************************************************************
Split a Python List into Evenly Sized Chunks
def split_list(lst, chunk_size):
return [lst[i:i + chunk_size] for i in range(0, len(lst), chunk_size)]

my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
chunk_size = 3

chunks = split_list(my_list, chunk_size)

# Output the result


print("Original List:", my_list)
print("List split into chunks:", chunks)

Example Input:
 Original list: [1, 2, 3, 4, 5, 6, 7, 8, 9].
 Chunk size: 3.
Output:
 Chunks: [[1, 2, 3], [4, 5, 6], [7, 8, 9]].
*****************************************************
Advantages of a binary search over a linear search
Binary search has several advantages over linear search, particularly when dealing with large,
sorted datasets.
 Faster Search Time
 Efficiency with Sorted Data
 Predictable Performance
 Optimal for Static Datasets

Example Comparison
Dataset: [1, 3, 5, 7, 9, 11, 13, 15]
Searching for 13:
 Linear Search:
o Comparisons: 1 → 3 → 5 → 7 → 9 → 11 → 13 (7 steps).
 Binary Search:
o Comparisons: 9 → 13 (2 steps).
 Binary Search is far superior in terms of performance for large, sorted datasets due to
its logarithmic time complexity and minimal number of comparisons.
 Linear Search is more flexible and can be used in unsorted data structures or small
datasets, but it is inefficient as the data grows in size.
***********************************************************
Sort a List in Python
The sort() method sorts the list in place, meaning it modifies the original list and doesn't return
a new list.
my_list = [3, 1, 4, 1, 5, 9, 2]
my_list.sort()
print(my_list) # Output: [1, 1, 2, 3, 4, 5, 9]
my_list.sort(reverse=True)

*******************************************************************
Types of Data Structures

*****************************************************************************
Time complexity and space complexity
Time complexity
Time complexity refers to the amount of time an algorithm takes to run as a function of the input
size. It helps to analyze how the runtime of an algorithm increases as the size of the input data
increases.
Space Complexity:
Space complexity refers to the amount of memory or space an algorithm needs to run as a function
of the input size. It measures the total memory consumed by an algorithm, including both the space
required for the input and any auxiliary space used during the algorithm's execution.
**********************************************************************
Python list and iterates using the range() function What will be the output?
arr = [1, 2, 3, 4, 5, 6]
for i in range(1, 6):
arr[i - 1] = arr[i]
for i in range(0, 6):
print(arr[i], end = " ")

Answer:
 Input List Initialization:
 First for Loop
Key Theoretical Observations:
 The last element (arr[5]) is not modified during the loop.
 The loop effectively shifts elements to the left by one position, and the last two elements
become identical.
Theoretical Concepts Involved
 List Mutability
 Index-Based Access
 Data Shifting
 Element Duplication
Final Output Explanation
 After both loops:
 The first loop shifts elements left, and the list becomes [2, 3, 4, 5, 6, 6].
 The second loop prints all elements of the modified list consecutively.
 The final output is: Output : 234566
******************************************************************
single inheritance
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
print(f"{self.name} makes a sound.")

class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
def make_sound(self):
print(f"{self.name}, the {self.breed}, barks.")

dog = Dog("Buddy", "Golden Retriever")


dog.make_sound() # Output: Buddy, the Golden Retriever, barks.
*************************************************************************
Hierarchical Inheritance
In hierarchical inheritance, multiple derived classes inherit from a single base class. This
design pattern offers several benefits, especially in scenarios where the derived classes share
common attributes and methods.
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound

def make_sound(self):
print(f"{self.name} makes a {self.sound} sound.")

class Dog(Animal):
def fetch(self):
print(f"{self.name} is fetching the ball.")

class Cat(Animal):
def climb(self):
print(f"{self.name} is climbing a tree.")

class Bird(Animal):
def fly(self):
print(f"{self.name} is flying.")
**********************************************************************
multiple subclasses like Car, Bike, and Truck
# Base class - Vehicle
class Vehicle:
def start(self):
print("The vehicle is starting.")

# Derived class - Car


class Car(Vehicle):
def display_info(self):
print("This is a car.")

# Derived class - Bike


class Bike(Vehicle):
def display_info(self):
print("This is a bike.")

# Create instances
car = Car()
bike = Bike()

# Call methods
car.start() # Output: The vehicle is starting.
car.display_info() # Output: This is a car.

bike.start() # Output: The vehicle is starting.


bike.display_info()# Output: This is a bike.
**************************************************************************
Difference between Linear and Non-linear Data Structures:
Feature Linear Data Structures Non-Linear Data Structures

Arrangement of
Sequentially, one after the other Hierarchical or network-based
Elements

Memory Allocation Contiguous memory allocation Dynamic memory allocation

Element Access Sequential or direct index access Traversal algorithms (DFS, BFS)

One-to-many or many-to-many
Relationships One-to-one (between adjacent elements)
relationships

Simple, sequential insertion, deletion, Complex, involves traversals,


Operations
and access hierarchical relations

Examples Arrays, Linked Lists, Stacks, Queues Trees, Graphs

Sequential data processing, task Hierarchical data representation,


Use Cases
management, expression evaluation network modeling

 Linear Data Structures are best for scenarios where elements are processed sequentially,
and the relationship between them is simple and direct.
 Non-Linear Data Structures are suited for applications where elements are
interconnected in a more complex way, such as in trees and graphs, representing
hierarchical or network relationships.
*************************************************************************
Abstraction
 Abstraction in OOP (Object-Oriented Programming) refers to the concept of hiding
the implementation details and showing only the necessary features of an object.
 It allows focusing on what an object does, rather than how it does it.
 In OOP, abstraction is achieved through: Abstract classes and Interfaces.

Encapsulation
 Encapsulation in Object-Oriented Programming (OOP) is the concept of bundling the
data (variables) and methods (functions) that operate on the data into a single unit known
as a class.
 It also refers to the practice of restricting access to some of an object's components,
thereby preventing unintended interference and misuse.
Encapsulation is achieved by using access modifiers such as:
 Private
 Public
 Protected
************************************************************************
Class and Object in OOP:
 Class: A class in Object-Oriented Programming (OOP) is a blueprint or template for
creating objects. It defines a set of attributes (variables) and methods (functions) that the
created objects will have. A class is essentially a user-defined data type that describes the
properties and behaviors of objects.
 Object: An object is an instance of a class. It is a concrete entity created using the class as
a template. Each object has its own attributes and methods defined by the class.
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
# Create objects (instances) of the Car class
car1 = Car("Toyota", "Corolla", 2020)
car2 = Car("Honda", "Civic", 2022)
************************************************************************
Self-parameter
 In Python, the self parameter refers to the instance of the class. It is a way for an object to
refer to itself within the class methods.
 The self parameter is used in instance methods to access and manipulate the instance's
attributes and other methods.
needed
1. Binding Methods to Objects:
o When you create an object, it has its own set of data (attributes). The self keyword
is needed to bind the method to the object (instance) and ensure it operates on the
object’s data, not on the class itself.
2. Referring to the Current Instance
Every instance of a class has its own state (data), and self refers to that state. It allows each
object to have its own set of attributes and behaviors, even if they come from the same
class.
class Car:
def __init__(self, make, model):
self.make = make
self.model = model
def display_info(self):
print(f"Car: {self.make} {self.model}")
*******************************************************************************

Inheritance
 Inheritance is one of the key concepts in Object-Oriented Programming (OOP) that allows
a class (called a subclass or derived class) to inherit attributes and methods from another
class (called a superclass or base class).
 This promotes code reuse and establishes a relationship between classes.
In Python, inheritance allows a subclass to:
 Inherit
 Override or extend
 Access
Types of Inheritance in Python: Single Inheritance, Multiple Inheritance, Multilevel
Inheritance, Hierarchical Inheritance and Hybrid Inheritance.
**************************************************************************
Exception Handling in Python and Java
Both Python and Java support exception handling to manage runtime errors, but their
approaches, syntax, and features differ in several ways.

Aspect Python Java

Syntax for Try- Python uses try and except


Java uses try and catch blocks.
Except blocks.

Python uses finally to define


Syntax for Java also uses finally for similar
code that runs regardless of
Finally Block functionality.
whether an exception occurred.

Catching Python allows catching specific Java allows catching specific exceptions and
Specific exceptions by listing them after supports hierarchical exception handling
Exceptions the except keyword. with inheritance.

Exception In Python, you can use raise to In Java, exceptions can be re-thrown using
Propagation re-throw an exception. throw or throws.

Python allows creating user-


Java also allows creating user-defined
User-Defined defined exceptions by
exceptions by extending Exception or
Exceptions subclassing the built-in
RuntimeException.
Exception class.

In Python, exceptions inherit Java has a more structured hierarchy where


Exception from the BaseException class, all exceptions inherit from Throwable,
Hierarchy which has Exception as a which has two main subclasses: Error and
subclass. Exception.

Catching In Python, you can catch


In Java, multiple exceptions can be caught
Multiple multiple exceptions in a single
using a multi-catch feature with `
Exceptions except block using a tuple.

*********************************************************************************
One-dimensional Array AND Multi-dimensional Array

One-dimensional Array
A one-dimensional array is a simple linear collection of elements. It is essentially a list of values
arranged in a single row or column.
# One-dimensional array
one_dim_array = [1, 2, 3, 4, 5]

# Accessing elements
print("First element:", one_dim_array[0]) # Output: 1
print("Last element:", one_dim_array[-1]) # Output: 5

Multi-dimensional Array
A multi-dimensional array, on the other hand, is an array of arrays. It represents data in a matrix
form, which can be two-dimensional, three-dimensional, or even higher-dimensional.
# Two-dimensional array (2D)
two_dim_array = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]

# Accessing elements
print("Element at row 1, column 1:", two_dim_array[0][0]) # Output: 1
print("Element at row 3, column 2:", two_dim_array[2][1]) # Output: 8

Key Differences:
Aspect One-dimensional Array Multi-dimensional Array

Structure A single list of elements. An array of arrays, representing a grid or matrix.

Dimensionality 1D (single row or column). 2D or more (rows and columns, or higher).


Aspect One-dimensional Array Multi-dimensional Array

Access elements by a single Access elements by multiple indices (e.g., row


Access
index. and column).

Example [1, 2, 3, 4, 5] [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

One-dimensional Array:
Advantages:
1. Simplicity
2. Efficient Memory Usage
3. Faster Access
4. Ease of Use
Disadvantages:
1. Limited Structure
2. Scalability
3. Limited Representation
Multi-dimensional Array:
Advantages:
1. Complex Data Representation
2. Efficient for Mathematical Operations
3. Organized Data
4. Support for Higher Dimensions
Disadvantages:
1. Complexity
2. Memory Usage
3. Slower Access
4. Overhead
************************************************************************8
List Methods:
Explanation:
1. append(): Adds an item (6) to the end of the list.
2. extend(): Adds elements [7, 8, 9] from another iterable.
3. insert(): Inserts item 10 at index 3.
4. remove(): Removes the first occurrence of the item 10.
5. pop(): Pops and removes the element at index 2, which is 3.
6. clear(): Clears the list, making it empty.
7. index(): Finds the index of the first occurrence of 3.
8. count(): Counts how many times 2 appears in the list.
9. sort(): Sorts the list in ascending order.
10. reverse(): Reverses the order of the list.
11. copy(): Creates a shallow copy of the list.
12. sort(reverse=True): Sorts the list in descending order.
13. reverse() again: Reverses the list again.

my_list = [1, 2, 3, 4, 5]

my_list.append(6)
print("After append:", my_list)

my_list.extend([7, 8, 9])
print("After extend:", my_list)

my_list.insert(3, 10) # Insert 10 at index 3


print("After insert:", my_list)

my_list.remove(10) # Removes first occurrence of 10


print("After remove:", my_list)

popped_item = my_list.pop(2) # Pops the item at index 2


print("After pop:", my_list)
print("Popped item:", popped_item)

my_list.clear()
print("After clear:", my_list)

my_list = [1, 2, 3, 4, 5]

index_of_item = my_list.index(3)
print("Index of 3:", index_of_item)

count_of_item = my_list.count(2)
print("Count of 2:", count_of_item)

my_list.sort()
print("After sort:", my_list)

my_list.reverse()
print("After reverse:", my_list)

copied_list = my_list.copy()
print("Copied list:", copied_list)

my_list.sort(reverse=True)
print("After sort with reverse=True:", my_list)

my_list.reverse()
print("After reverse again:", my_list)
Output :
After append: [1, 2, 3, 4, 5, 6]
After extend: [1, 2, 3, 4, 5, 6, 7, 8, 9]
After insert: [1, 2, 3, 10, 4, 5, 6, 7, 8, 9]
After remove: [1, 2, 3, 4, 5, 6, 7, 8, 9]
After pop: [1, 2, 4, 5, 6, 7, 8, 9]
Popped item: 3
After clear: []
Index of 3: 2
Count of 2: 1
After sort: [1, 2, 3, 4, 5]
After reverse: [5, 4, 3, 2, 1]
Copied list: [5, 4, 3, 2, 1]
After sort with reverse=True: [5, 4, 3, 2, 1]
After reverse again: [1, 2, 3, 4, 5]
***************************************************************************8

You might also like