0% found this document useful (0 votes)
22 views43 pages

FP Final Key-2

The document provides a comprehensive overview of functional programming (FP) concepts, including pure functions, higher-order functions, and immutability, along with practical examples in Python. It also compares FP with object-oriented programming (OOP), highlighting advantages and disadvantages of both paradigms in large-scale software development. Additionally, it discusses Python's string methods and tuple immutability, demonstrating how these features support functional programming principles.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
22 views43 pages

FP Final Key-2

The document provides a comprehensive overview of functional programming (FP) concepts, including pure functions, higher-order functions, and immutability, along with practical examples in Python. It also compares FP with object-oriented programming (OOP), highlighting advantages and disadvantages of both paradigms in large-scale software development. Additionally, it discusses Python's string methods and tuple immutability, demonstrating how these features support functional programming principles.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 43

FP FINAL KEY

Short Answers:
Q1. What is functional programming ?
A. Functional programming (FP) is a programming paradigm where you build software by
composing pure functions, avoiding shared state and mutable data. The key idea is to treat
computation as the evaluation of mathematical functions and avoid changing state or
producing side effects.

Here are some core concepts of functional programming:

1.​ Pure Functions


2.​ First-Class Functions
3.​ Immutability
4.​ Higher-Order Functions
5.​ Referential Transparency
6.​ Lazy Evaluation.

Q2. Identify the Python method used to add a new key-value pair to the dictionary.
A. In Python, the method used to add a new key-value pair to a dictionary is the update()
method or by simply using the square bracket ([]) notation.

Here’s how both methods work:

1.​ Using square bracket notation:

my_dict = {'a': 1, 'b': 2}


my_dict['c'] = 3 # Adds a new key 'c' with the value 3
print(my_dict)

Output:
{'a': 1, 'b': 2, 'c': 3}

2. Using the update() method:

my_dict = {'a': 1, 'b': 2}


my_dict.update({'c': 3}) # Adds key 'c' with value 3
print(my_dict)

Output:
{'a': 1, 'b': 2, 'c': 3}

Q3. Given the statement import math, which function would you use to calculate the
square root of the number?
A. After importing the math module in Python, you can use the math.sqrt() function to
calculate the square root of a number.

For example:

import math

result = math.sqrt(16)

print(result)

Output: 4.0

The math.sqrt() function returns the square root of the given number. If the number is
negative, it will raise a ValueError.

Q4. Explain the difference between a try block and an except block in Python.

A.

1. Try Block:

●​ The try block is used to wrap the code that you want to test for exceptions (errors).
●​ You place the code that may raise an error inside the try block. If no error occurs,
the program continues as normal.

2. Except Block:

●​ The except block is used to handle the exception (error) that was raised in the try
block.
●​ It "catches" the exception and allows you to handle it (e.g., print a custom message,
log the error, or perform an alternative action).

Q5. What Python module is commonly used to send emails using the SMTP protocol?

A. In Python, the smtplib module is commonly used to send emails using the SMTP
(Simple Mail Transfer Protocol). This module provides a simple interface for sending email
to an SMTP server.

Q6. Given a database connection, which Python module function is used to execute a
SQL query?

A. In Python, when working with a database connection (like with SQLite, MySQL,
PostgreSQL, etc.), the execute() method is commonly used to run a SQL query.

●​ cursor.execute(query) is used to execute a SQL query. The query can be a string


containing the SQL statement you want to run.
●​ cursor.fetchall() can be used to retrieve all rows returned by the query, while
cursor.fetchone() can fetch a single row.

Q7. Explain the concept of higher-order functions in haskell.

A. In Haskell, higher-order functions (HOFs) are functions that either:

1.​ Take one or more functions as arguments, or


2.​ Return a function as a result.

Higher-Order Functions

1.​ Functions as Arguments: A higher-order function can accept other functions as


parameters. This allows you to create more abstract and reusable code.

2. Functions as Return Values: A higher-order function can return another function as its
result. This is often used in function composition or partially applied functions.

Q8. How does modularity in Haskell improve the maintainability of large codebases?

A. Modularity in Haskell improves maintainability by breaking code into small, reusable, and
focused components. Key benefits include:

1.​ Pure Functions:

2.​ Separation of Concerns:

3.​ Type System:

4.​ Abstraction:
5.​ Lazy Evaluation:
6.​ Declarative Style:
7.​ Concurrency:
8.​ Package Management:

Q9. What does Lisp stand for in programming?

A. In programming, Lisp stands for LISt Processing. It is a family of programming


languages that was originally designed for symbolic computation and artificial intelligence
research.

Q10. Explain the role of recursion in LISP programming.

A. Recursion plays a central role in Lisp programming due to the language’s functional
nature and its emphasis on manipulating recursive data structures like lists. In Lisp, recursion
is often used as the primary mechanism for iteration and defining repetitive tasks.
Long Answers:

Q11. Write a Python program to demonstrate the usage of the map() and filter()
functions. Explain how these functions align with functional programming principles.

A. Here's a Python program that demonstrates the usage of the map() and filter()
functions:

# Using map() to apply a function to each element in a list

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

# Define a function to square a number

def square(x):

return x ** 2

# Apply the square function to each element in the list using map()

squared_numbers = list(map(square, numbers))

print("Squared numbers:", squared_numbers)

# Using filter() to filter out even numbers from a list

def is_even(x):

return x % 2 == 0

# Filter the even numbers from the list using filter()

even_numbers = list(filter(is_even, numbers))

print("Even numbers:", even_numbers)

Output:

Squared numbers: [1, 4, 9, 16, 25]

Even numbers: [2, 4]

Alignment with Functional Programming Principles:

1.​ First-Class Functions:


○​ Both map() and filter() are examples of first-class functions in Python,
meaning they can accept other functions as arguments. This aligns with
functional programming's principle that functions are first-class citizens,
which can be passed around as values.
2.​ Higher-Order Functions:
○​ Both map() and filter() are higher-order functions. They take other
functions (square and is_even) as arguments and return new iterables
based on applying those functions. This is a key principle of functional
programming, where you abstract behavior using functions.
3.​ Immutability:
○​ Both map() and filter() return new iterables without modifying the
original data (numbers list). This aligns with the principle of immutability in
functional programming, where data is not changed directly but instead new
data structures are created based on the original data.
4.​ Declarative Style:
○​ Using map() and filter() allows you to express the desired outcome
("square each number" or "filter even numbers") in a declarative style. Instead
of explicitly iterating over elements, you describe what you want to achieve,
and the functions take care of the underlying iteration, which is a hallmark of
functional programming.
5.​ Purity:
○​ Both functions can be pure in nature. If the square() and is_even()
functions are pure (i.e., they do not have side effects and always return the
same output for the same input), then the use of map() and filter()
promotes a functional programming style that avoids side effects.

Q12. Analyze the advantages and disadvantages of using functional programming over
object-oriented programming in large-scale software development.

A. When comparing functional programming (FP) and object-oriented programming


(OOP) for large-scale software development, both paradigms offer distinct advantages and
disadvantages. The choice between them largely depends on the nature of the project, the
team's familiarity with the paradigm, and the specific goals of the development process.
Below is an analysis of the advantages and disadvantages of using functional programming
(FP) over object-oriented programming (OOP) in large-scale software development.

Advantages of Functional Programming in Large-Scale Software Development

1.​ Immutability and Predictability:​

○​ Advantage: In FP, data is typically immutable, meaning once it’s created, it


cannot be changed. This leads to more predictable code since functions don’t
alter external states. This reduces bugs related to side effects and helps
maintain consistent behavior, especially in large and complex systems.
2.​ Concurrency and Parallelism:​

○​ Advantage: Functional programming facilitates easier concurrency and


parallelism because functions are independent of mutable state. This means
multiple threads or processes can safely execute without interfering with each
other.
3.​ Simplified Testing and Debugging:​

○​ Advantage: Since FP encourages the use of pure functions (functions that


always return the same output for the same input and have no side effects), it
becomes much easier to test and debug code. You can test individual
functions in isolation without worrying about how they affect the rest of the
system.
4.​ Modularity and Reusability:​

○​ Advantage: FP encourages modularity through the use of small, reusable


functions and higher-order functions. This makes the codebase easier to
maintain and extend over time.
5.​ Declarative Nature:​

○​ Advantage: FP tends to be more declarative compared to OOP, meaning you


describe what should be done rather than how it should be done. This leads to
cleaner, more expressive code that is often easier to understand and maintain.
6.​ Mathematical Foundations:​

○​ Advantage: FP is closely aligned with mathematical logic, particularly


lambda calculus and category theory, which gives rise to a more rigorous
and structured way of thinking about programs. This can help when reasoning
about complex algorithms or systems.

Disadvantages of Functional Programming in Large-Scale Software Development

1.​ Learning Curve:​

○​ Disadvantage: FP has a steeper learning curve for developers, especially


those coming from an OOP background. Concepts like monads, laziness, and
type theory can be difficult to grasp and may hinder productivity in the early
stages of a project.
2.​ Performance Concerns:​

○​ Disadvantage: While FP often uses immutable data and functions that don't
modify state, this can lead to performance overheads. Creating new copies of
data structures can be inefficient in cases where large amounts of data need to
be updated frequently.
3.​ Complexity with Side Effects:​

○​ Disadvantage: While FP avoids side effects in pure functions, in many


large-scale applications, side effects are inevitable (e.g., I/O operations,
database access). Handling side effects cleanly in FP can be complex, often
requiring tools like monads (in languages like Haskell).
4.​ Limited Ecosystem and Libraries:​

○​ Disadvantage: Compared to OOP languages like Java, Python, or C#, FP


languages have a smaller ecosystem of libraries and frameworks, especially
for certain types of applications (e.g., GUI development or mobile apps).
5.​ Tooling and Debugging:​

○​ Disadvantage: Debugging and tracing the flow of functional programs can be


more difficult, especially when recursion and higher-order functions are
heavily used. The call stack may become deep, and it can be harder to trace
errors in large functional systems.

Advantages of Object-Oriented Programming in Large-Scale Software


Development

1.​ Natural Mapping to Real-World Concepts:​

○​ Advantage: OOP’s use of objects and classes makes it easier to model


real-world entities and behaviors, which can be beneficial for large systems
with complex data and interactions.
2.​ Better Support for State:​

○​ Advantage: OOP’s emphasis on encapsulating state within objects makes it


easier to manage mutable state, especially in interactive applications (e.g.,
GUIs or games) where objects often represent real-world entities that need to
change over time.
.
3.​ Wide Ecosystem and Tools:​

○​ Advantage: OOP languages typically have a larger set of libraries, tools, and
frameworks that are specifically built for large-scale software development.
4.​ Easier for Teams to Work Together:​
○​ Advantage: OOP’s clear division of responsibilities between classes and
objects makes it easier for teams to work collaboratively, as different team
members can work on different objects or modules without much overlap.

Disadvantages of Object-Oriented Programming in Large-Scale Software


Development

1.​ Complexity and Overhead:​

○​ Disadvantage: In large systems, the use of numerous interrelated objects can


lead to complexity and tight coupling. This can make the code harder to
maintain and extend as the system grows.
2.​ Inheritance Issues:​

○​ Disadvantage: While inheritance is a core feature of OOP, deep inheritance


hierarchies can make the code harder to modify, understand, and maintain.
3.​ State Management:​

○​ Disadvantage: Managing state in OOP can lead to problems like shared


mutable state and side effects, especially in multithreaded environments,
which can introduce bugs.

Q13. Explain the immutability of tuples in python. How does this property align with
functional programming? Provide examples using tuple methods like Count( ) and
Index( ).

A. Immutability of Tuples in Python

In Python, tuples are immutable sequences, meaning that once a tuple is created, its
elements cannot be modified, added, or removed. This immutability property is one of the
key characteristics that sets tuples apart from lists, which are mutable.

How Immutability Aligns with Functional Programming

Immutability is a core principle of functional programming (FP). In FP, data is treated as


immutable, and functions should avoid changing the state of the data. This has several
advantages:

1.​ Predictability: Since data can't change unexpectedly, the behavior of the program is
more predictable.
2.​ Safety: Avoiding side effects (i.e., changes to shared state) makes the code safer,
particularly in multi-threaded or concurrent environments.
3.​ Referential Transparency: Functions in FP should always return the same output for
the same input. Immutability helps enforce this behavior.

Tuple Methods: count() and index()

1. count()

The count() method returns the number of times a specific element appears in the tuple. It
does not modify the tuple, which is consistent with the idea of immutability.

Example of count():

# Creating a tuple

my_tuple = (1, 2, 3, 2, 4, 2)

# Using count() to find the occurrences of '2'

count_of_2 = my_tuple.count(2)

print("Count of 2:", count_of_2) # Output: 3

2. index()

The index() method returns the index of the first occurrence of a specified element. It
doesn't alter the tuple, reinforcing the idea of immutability.

Example of index():

# Creating a tuple

my_tuple = (10, 20, 30, 40, 50)

# Using index() to find the index of the element '30'

index_of_30 = my_tuple.index(30)

print("Index of 30:", index_of_30) # Output: 2

Q14. How do Python string methods like split(), join(), and replace support functional
programming? Write a Python program to process a paragraph of text using these
methods.

A. Python string methods like split(), join(), and replace() align well with
functional programming principles. Here's how:
1.​ split(): This method splits a string into a list of substrings based on a delimiter. ​

2.​ join(): This method joins elements of an iterable (like a list or tuple) into a single
string, separating them with a specified delimiter. ​

3.​ replace(): This method creates a new string by replacing occurrences of a


specified substring with another substring. ​

Python Program to Process a Paragraph Using These Methods

Let’s write a Python program that processes a paragraph of text using the split(),
join(), and replace() methods.

Example Program:

# Sample paragraph of text

paragraph = """Functional programming emphasizes immutability,

higher-order functions, and recursion. It avoids side effects

and mutable data, which makes programs easier to test and reason about."""

# Step 1: Split the paragraph into sentences (based on periods)

sentences = paragraph.split('.')

# Step 2: Replace specific words (e.g., 'immutability' with 'purity')

modified_sentences = [sentence.replace('immutability', 'purity') for sentence in sentences]

# Step 3: Join the sentences back into a single paragraph

modified_paragraph = '. '.join(modified_sentences)

# Print the original and modified paragraphs

print("Original Paragraph:")

print(paragraph)

print("\nModified Paragraph:")

print(modified_paragraph)

Output:
Original Paragraph:

Functional programming emphasizes immutability,

higher-order functions, and recursion. It avoids side effects

and mutable data, which makes programs easier to test and reason about.

Modified Paragraph:

Functional programming emphasizes purity,

higher-order functions, and recursion. It avoids side effects

and mutable data, which makes programs easier to test and reason about.

Q15. Explain the concept of exception handling in python. Provide an example to


handle multiple exceptions using a try-except block.

A. Exception Handling in Python

Exception handling in Python is a mechanism that allows you to manage errors gracefully
without interrupting the normal flow of a program. When an error occurs (an exception is
raised), the program can catch that error and handle it in a controlled way using a
try-except block.

Python's exception handling is built around the try, except, else, and finally
blocks:

1.​ try: Code that might raise an exception is placed inside the try block.
2.​ except: If an exception occurs in the try block, Python will look for an appropriate
except block to handle the exception.
3.​ else: If no exception occurs in the try block, the code inside the else block will
execute.
4.​ finally: This block will execute no matter what, whether or not an exception
occurred. It's typically used for cleanup actions like closing files or releasing
resources.

Handling Multiple Exceptions

Python allows you to handle multiple exceptions in several ways. You can:

1.​ Specify different except blocks for different exception types.


2.​ Catch multiple exceptions in a single except block by specifying a tuple of
exception types.
Example: Handling Multiple Exceptions Using a Try-Except Block

Let’s consider an example where we handle multiple exceptions, including ValueError,


ZeroDivisionError, and FileNotFoundError.

def divide_numbers():

try:

# Input values

num1 = int(input("Enter the first number: "))

num2 = int(input("Enter the second number: "))

# Perform division

result = num1 / num2

print("The result is:", result)

except ZeroDivisionError:

print("Error: Cannot divide by zero!")

except ValueError:

print("Error: Invalid input! Please enter valid integers.")

except Exception as e:

print(f"An unexpected error occurred: {e}")

finally:

print("Execution completed.")

# Call the function

divide_numbers()

Example Output:

Case 1: Valid Input

Enter the first number: 10

Enter the second number: 2


The result is: 5.0

Execution completed.

Case 2: Division by Zero

Enter the first number: 10

Enter the second number: 0

Error: Cannot divide by zero!

Execution completed.

Case 3: Invalid Input (Non-integer)

Enter the first number: 10

Enter the second number: abc

Error: Invalid input! Please enter valid integers.

Execution completed.

Q16. Analyze the advantage of using tuple methods over list methods for storing and
retrieving immutable data. Provide examples of key tuple methods like index() and
count().

A. Advantages of Using Tuple Methods Over List Methods for Storing and Retrieving
Immutable Data

Tuples and lists are both sequence data types in Python, but they serve different purposes and
have distinct characteristics. The key advantage of using tuple methods over list methods
when working with immutable data lies in their immutability property, which provides
several benefits:

1. Immutability Ensures Data Integrity:

Advantage over Lists: Lists are mutable, so their data can be modified after creation, leading
to potential issues like unintentional changes or side effects. Tuples, on the other hand,
prevent such modifications, making them more reliable in scenarios requiring constant data.

2. Faster Performance for Fixed Data:

Advantage over Lists: Lists incur overhead due to their dynamic nature, which allows
modification of data (adding/removing elements). This makes lists less efficient than tuples
when dealing with large datasets that do not need modification.

3. Tuple Methods for Efficient Data Retrieval:


Tuples provide several useful methods, such as index() and count(), that help with
efficient data retrieval. These methods work efficiently on tuples without modifying the
underlying data, aligning perfectly with the idea of working with immutable data.

Key Tuple Methods for Storing and Retrieving Immutable Data

1. index() Method:

The index() method returns the index of the first occurrence of a specified value in the
tuple. If the value is not found, it raises a ValueError.

Example:

my_tuple = (10, 20, 30, 40, 50)

# Find the index of element 30

index_of_30 = my_tuple.index(30)

print("Index of 30:", index_of_30)

# Output: Index of 30: 2

2. count() Method:

The count() method returns the number of occurrences of a specified element in the tuple.

Example:

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

# Count the number of occurrences of '2'

count_of_2 = my_tuple.count(2)

print("Count of 2:", count_of_2)

# Output: Count of 2: 3

Q17. Evaluate the advantages of file modes (r, w, a, rb, etc.) in Python's file handling.
provide examples to demonstrate their usage.
A. Advantages of Different File Modes

r (Read): Simple and effective for reading content from existing files.

w (Write): Useful for creating or overwriting files, ensuring you start with fresh content.

a (Append): Keeps existing content intact while adding new data, ideal for log files or
growing data.

rb, wb, ab (Binary Modes): Essential for handling binary data (e.g., images, videos), where
text encoding doesn’t apply.

r+, w+ (Read/Write Modes): Combine reading and writing capabilities, enabling in-place
modification of file contents.

x (Exclusive Creation): Prevents overwriting existing files, ensuring safe creation of new
files.

Examples of File Modes Usage

1. Using r (Read Mode)

# Opening a file in read mode

with open('example.txt', 'r') as file:

content = file.read()

print(content)

●​ Use Case: Read the content of example.txt.

2. Using w (Write Mode)

# Opening a file in write mode

with open('newfile.txt', 'w') as file:

file.write("This is a new file with some content.")

●​ Use Case: Overwrite or create a new file newfile.txt with the specified content.

3. Using a (Append Mode)


# Opening a file in append mode

with open('existingfile.txt', 'a') as file:

file.write("\nThis line is appended to the existing file.")

●​ Use Case: Append content to existingfile.txt without overwriting its current


content.

4. Using rb (Read Binary Mode)

# Reading a binary file (e.g., an image)

with open('image.jpg', 'rb') as file:

data = file.read()

print(type(data)) # <class 'bytes'>

●​ Use Case: Read binary data from image.jpg.

5. Using wb (Write Binary Mode)

# Writing binary data to a file

with open('output.bin', 'wb') as file:

binary_data = b'\x00\x01\x02\x03'

file.write(binary_data)

●​ Use Case: Write binary data to output.bin.

6. Using r+ (Read and Write Mode)

# Reading and modifying an existing file

with open('example.txt', 'r+') as file:

content = file.read()

print("Original Content:", content)

file.seek(0)

file.write("Updated content of the file.")


●​ Use Case: Read and modify the content of example.txt without overwriting the
entire file.

7. Using w+ (Write and Read Mode)

# Overwriting and modifying a file

with open('newfile.txt', 'w+') as file:

file.write("New content")

file.seek(0)

print(file.read()) # Reading back the content after writing

●​ Use Case: Overwrite and read back the content of the file.

8. Using x (Exclusive Creation Mode)

# Creating a new file exclusively

try:

with open('exclusive.txt', 'x') as file:

file.write("This file is created exclusively.")

except FileExistsError:

print("File already exists!")

●​ Use Case: Ensure the file exclusive.txt is created only if it doesn't already
exist.

Q18. Explain the role of File IO in Python.Write a program to append a user-provided


string to an existing file and then read and print its contents.

A. Role of File I/O in Python

File Input/Output (I/O) in Python refers to the process of reading data from files and writing
data to files. Python provides built-in support for interacting with files through a series of
functions and methods.
File I/O Methods in Python:

●​ open(): Opens a file and returns a file object.


●​ read(): Reads the content of the file.
●​ write(): Writes content to the file.
●​ append(): Appends data to an existing file.
●​ close(): Closes the file after operations are completed.

Append and Read from File Example

The following Python program demonstrates how to append a user-provided string to an


existing file and then read and print its contents.

Program: Append a String to a File and Read Its Contents

def append_and_read_file(filename):

# Step 1: Get input from the user

user_input = input("Enter a string to append to the file: ")

# Step 2: Open the file in append mode to add data to it

with open(filename, 'a') as file:

file.write(user_input + '\n') # Appending the input with a newline

# Step 3: Open the file in read mode to print the content

with open(filename, 'r') as file:

content = file.read() # Read the entire content of the file

# Step 4: Display the updated content of the file

print("\nUpdated content of the file:")

print(content)

# Example usage: Assuming 'example.txt' is an existing file.

filename = 'example.txt'

append_and_read_file(filename)
Output After Execution: The contents of the file example.txt will be updated and
printed:​
Hello, this is the first line.

Welcome to Python file handling.

This is the new content.

Q19. Explain the concept of sending emails in Python. explain the concept of smtplib
module and provide an example to send a simple email without attachments.

A. Sending Emails in Python

Sending emails in Python can be done using the smtplib module, which is a built-in
Python library designed to interact with SMTP (Simple Mail Transfer Protocol) servers.
SMTP is the protocol used to send emails over the internet. The smtplib module allows
Python programs to send emails via an SMTP server, and it supports sending both plain-text
and HTML emails. It can also be used to send emails with attachments, although that requires
additional modules like email.

Key Concepts of smtplib

●​ SMTP (Simple Mail Transfer Protocol): SMTP is the standard protocol for sending
emails. It is used by email servers to send email messages to each other.
●​ SMTP Server: An SMTP server is used to send emails, such as Gmail's SMTP server
(smtp.gmail.com), which allows you to send emails through a Gmail account.
●​ Authentication: When sending emails, authentication is required to ensure that only
authorized users can send messages from an email address.

The smtplib module in Python provides the following key features:

1.​ smtplib.SMTP(): Creates an SMTP client session object.


2.​ smtp.login(): Authenticates the user to the SMTP server.
3.​ smtp.sendmail(): Sends an email.
4.​ smtp.quit(): Terminates the session with the SMTP server.

Example of Sending a Simple Email Using smtplib

Let's send a basic plain-text email using Gmail’s SMTP server (smtp.gmail.com). The
program will connect to the SMTP server, authenticate the sender’s email account, and send a
simple email message.

Program: Sending a Simple Email without Attachments


import smtplib

from email.mime.text import MIMEText

from email.mime.multipart import MIMEMultipart

def send_email():

# Email sender and receiver details

sender_email = "[email protected]"

receiver_email = "[email protected]"

password = "your_email_password" # Your email account password (for Gmail, you may
need an app-specific password)

# Creating the MIME object for the email

msg = MIMEMultipart()

msg['From'] = sender_email

msg['To'] = receiver_email

msg['Subject'] = 'Test Email from Python'

# Email body content

body = "Hello, this is a test email sent from Python using smtplib."

# Attach the body with the email

msg.attach(MIMEText(body, 'plain'))

# Connecting to Gmail's SMTP server and sending the email

try:

# Set up the SMTP server connection (Gmail's SMTP server)

server = smtplib.SMTP('smtp.gmail.com', 587)

server.starttls() # Encrypts the connection

server.login(sender_email, password) # Log in to the email account

text = msg.as_string() # Convert the MIME message to string


server.sendmail(sender_email, receiver_email, text) # Send the email

print("Email sent successfully!")

except Exception as e:

print(f"Error occurred: {e}")

finally:

# Quit the server session

server.quit()

# Call the function to send the email

send_email()

Example Output:

If the email is sent successfully, the program will print:

Email sent successfully!

Q20. Evaluate the advantages of using generators over iterators in Python. Write a
generator function to yield prime numbers below a given limit and explain its efficiency.

A. Advantages of Using Generators Over Iterators in Python

Both generators and iterators in Python are tools that allow you to iterate over a sequence of
items. However, there are significant advantages to using generators, especially in terms of
efficiency and ease of use.

Here are the main advantages of using generators over iterators:

1.​ Lazy Evaluation:​

○​ Generators produce values lazily, meaning they only compute and yield a
value when required (on demand), whereas iterators typically require all
elements to be generated or stored upfront. This makes generators much more
memory efficient, especially when dealing with large datasets or infinite
sequences.
2.​ Memory Efficiency:​

○​ Since generators only yield one item at a time and do not store the entire
collection in memory, they are ideal for iterating over large data or sequences
that might be too large to fit in memory at once. On the other hand, iterators
might need to load the entire dataset into memory, which can be inefficient for
large datasets.
3.​ Simpler and Cleaner Code:​

○​ Generators are easier to implement and read. You can write a generator
function using the yield keyword, which simplifies the code for sequences
that need to generate values on the fly. Iterators, on the other hand, require
more boilerplate code with the implementation of methods like __iter__()
and __next__().
4.​ Performance:​

○​ Since generators don’t store all items at once, they tend to have better
performance when working with large or infinite datasets because they only
compute the next value as needed, avoiding redundant computations or
memory overhead.

Generator for Prime Numbers Below a Given Limit

Let’s write a generator function to yield prime numbers below a given limit. We'll also
explain its efficiency.

Prime Number Generator Function

A.​ def prime_generator(limit):


B.​ """A generator function to yield prime numbers below a given limit."""
C.​ # We start checking from 2 as 1 is not a prime number
D.​ num = 2
E.​ while num < limit:
F.​ # Check if 'num' is prime
G.​ is_prime = True
H.​ for i in range(2, int(num ** 0.5) + 1): # Check divisibility up to sqrt(num)
I.​ if num % i == 0:
J.​ is_prime = False
K.​ break
L.​ if is_prime:
M.​ yield num # Yield the prime number
N.​ num += 1
O.​
P.​ # Example usage
Q.​ limit = 50
R.​ prime_numbers = prime_generator(limit)
S.​
T.​ print(f"Prime numbers below {limit}:")
U.​ for prime in prime_numbers:
V.​ print(prime)

Example Output:

For limit = 50, the output will be:

W.​ Prime numbers below 50:


X.​ 2
Y.​ 3
Z.​ 5
AA.​ 7
BB.​ 11
CC.​ 13
DD.​ 17
EE.​19
FF.​23
GG.​ 29
HH.​ 31
II.​ 37
JJ.​ 41
KK.​ 43
LL.​47

Efficiency:

1.​ Memory Efficiency:​

○​ The generator doesn't store the entire list of primes in memory. It only
computes one prime at a time and yields it, making it highly memory efficient,
especially when working with larger values of limit.
2.​ Computational Efficiency:​

○​ The use of yield ensures that each prime number is calculated only when
requested. This is a more efficient approach compared to building an entire list
of primes upfront.
○​ Additionally, the primality test uses the square root approach, which makes the
primality check more efficient than checking divisibility up to the number
itself.
3.​ Lazy Evaluation:​

○​ The prime numbers are generated lazily, so if we only need a few primes (e.g.,
for the first few iterations of the loop), the function won't compute and yield
the entire sequence. This improves performance when dealing with larger
limits.

Q21. Write a Python program to connect to an SQLite database, create a table, insert
some records, and fetch the data. Explain the usage of sqlite3 module.

A.

SQLite Python Program: Create Table, Insert Records, and Fetch Data

Here’s a step-by-step Python program that connects to an SQLite database, creates a table,
inserts some records, and fetches the data.

import sqlite3

# Connect to the SQLite database (or create it if it doesn't exist)

conn = sqlite3.connect('example.db')

# Create a cursor object using the connection

cursor = conn.cursor()

# Step 1: Create a table

cursor.execute('''

CREATE TABLE IF NOT EXISTS users (

id INTEGER PRIMARY KEY AUTOINCREMENT,

name TEXT NOT NULL,

age INTEGER NOT NULL

''')

# Step 2: Insert some records into the table

cursor.execute("INSERT INTO users (name, age) VALUES ('Alice', 30)")

cursor.execute("INSERT INTO users (name, age) VALUES ('Bob', 25)")

cursor.execute("INSERT INTO users (name, age) VALUES ('Charlie', 35)")

# Commit the changes to the database


conn.commit()

# Step 3: Fetch the data from the table

cursor.execute("SELECT * FROM users")

rows = cursor.fetchall()

# Print the fetched records

print("Records from the 'users' table:")

for row in rows:

print(row)

# Close the connection

conn.close()

Output:

Records from the 'users' table:

(1, 'Alice', 30)

(2, 'Bob', 25)

(3, 'Charlie', 35)

How the sqlite3 Module is Used:

1.​ sqlite3.connect(): Establishes a connection to the SQLite database. ​

2.​ conn.cursor(): Creates a cursor object, which is used to interact with the
database and execute SQL queries.​

3.​ cursor.execute(): Executes SQL statements. ​

4.​ conn.commit(): Commits the transaction.​

5.​ cursor.fetchall(): Fetches all the rows from the result of a SELECT query
and returns them as a list of tuples.​

6.​ conn.close(): Closes the database connection to free up resources.​


Q22. Describe the key features of object-oriented programming (OOP). in Python.
Provide examples to demonstrate the concepts of inheritance and polymorphism.

A. Key Features of Object-Oriented Programming (OOP) in Python

Object-Oriented Programming (OOP) is a programming paradigm that uses "objects" and


"classes" to organize code in a way that models real-world entities. Python, being an
object-oriented language, supports all the key features of OOP, which include:

1.​ Classes and Objects:​

○​ Class: A class is a blueprint for creating objects. It defines the properties


(attributes) and behaviors (methods) that the objects created from the class
will have.
○​ Object: An object is an instance of a class. It contains data (attributes) and
functions (methods) defined in the class.

2.​ Inheritance:​

○​ Inheritance allows a class to inherit properties and behaviors (methods) from


another class. This promotes code reuse and creates a hierarchical class
structure.

Example:​

# Parent class

class Animal:

def __init__(self, name):

self.name = name

def speak(self):

return f"{self.name} makes a sound."

# Child class inheriting from Animal class

class Dog(Animal):

def speak(self): # Overriding the parent method

return f"{self.name} barks."


class Cat(Animal):

def speak(self): # Overriding the parent method

return f"{self.name} meows."

# Creating instances of the child classes

dog = Dog("Buddy")

cat = Cat("Whiskers")

print(dog.speak()) # Output: Buddy barks.

print(cat.speak()) # Output: Whiskers meows.

3.​ Polymorphism:​

○​ Polymorphism allows different classes to be treated as instances of the same


class through inheritance.
.

Example:​

# Parent class

class Shape:

def area(self):

pass

# Child class Square

class Square(Shape):

def __init__(self, side):

self.side = side

def area(self):

return self.side ** 2

# Child class Circle

class Circle(Shape):

def __init__(self, radius):


self.radius = radius

def area(self):

return 3.14 * (self.radius ** 2)

# Polymorphism in action

shapes = [Square(4), Circle(3)]

for shape in shapes:

print(f"Area: {shape.area()}") # Calls the appropriate area method

Output:​

Area: 16

Area: 28.26

Q23. Explain the concept of lazy evaluation in Haskell. Provide an example program to
demonstrate how lazy evaluation handles infinite lists.

A. Lazy Evaluation in Haskell

Lazy evaluation is a powerful feature in Haskell that allows computations to be deferred


until their values are actually needed. In simple terms, Haskell does not evaluate expressions
until their results are required, meaning that calculations are only performed when needed,
which can help avoid unnecessary work and enable the handling of infinite structures.

How Lazy Evaluation Handles Infinite Lists

Because of lazy evaluation, Haskell can handle infinite lists or streams efficiently. An
infinite list is only evaluated as needed. For example, you can define an infinite list of
numbers like [1, 2, 3, 4, 5, ...], but Haskell will only calculate as many numbers
from that list as are actually required by the program.

Example: Lazy Evaluation with Infinite Lists

Let's demonstrate lazy evaluation with an infinite list of natural numbers, and we will fetch
only the first few elements from the list.

-- Define an infinite list of natural numbers

naturals :: [Integer]

naturals = [1..] -- This is an infinite list starting from 1


-- Get the first 10 natural numbers from the infinite list

firstTenNaturals :: [Integer]

firstTenNaturals = take 10 naturals

-- Main function to print the first 10 numbers

main :: IO ()

main = print firstTenNaturals

Output:

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

Q24. Write a Haskell program to demonstrate the use of higher-order functions. Use the
functions to process a list of integers.

A. Haskell Program to Demonstrate Higher-Order Functions

In Haskell, higher-order functions are functions that take one or more functions as arguments
or return a function as a result. Higher-order functions are a fundamental feature of functional
programming, and they allow us to abstract common patterns of computation, making the
code more concise and reusable.

Let's write a Haskell program to demonstrate the use of higher-order functions by processing
a list of integers.

We'll use the following higher-order functions:

1.​ map
2.​ filter
3.​ foldr (fold right)

Example Program

-- A higher-order function that processes a list of integers

processList :: [Int] -> Int

processList lst = foldr (+) 0 (filter even (map (*2) lst))

-- Main function to run the program

main :: IO ()

main = do
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

let result = processList numbers

putStrLn $ "Processed result: " ++ show result

Output:

Processed result: 30

Q25. Create a Haskell program to calculate the factorial of a number using both
recursion and a higher-order function. Compare the approaches.

A. Haskell Program to Calculate Factorial Using Recursion and Higher-Order Functions:

In Haskell, factorial can be calculated using both recursion (which is a fundamental concept
in functional programming) and higher-order functions. We will demonstrate both
approaches and compare them.

Approach 1: Factorial Using Recursion

In the recursive approach, we define the factorial function by reducing the problem to smaller
subproblems. The base case is when the number is 0 or 1, where the factorial is 1. For any
other number n, the factorial is n * factorial(n - 1).

Approach 2: Factorial Using a Higher-Order Function

In this approach, we will use the foldr function to accumulate the product of numbers from
1 to n. The foldr function is a higher-order function that recursively processes the list and
accumulates a result.

Haskell Code

-- Approach 1: Factorial using Recursion

factorialRecursive :: Integer -> Integer

factorialRecursive 0 = 1

factorialRecursive n = n * factorialRecursive (n - 1)

-- Approach 2: Factorial using Higher-Order Function (foldr)


factorialFoldr :: Integer -> Integer

factorialFoldr n = foldr (*) 1 [1..n]

-- Main function to test both approaches

main :: IO ()

main = do

let num = 5

let resultRec = factorialRecursive num

let resultFoldr = factorialFoldr num

putStrLn $ "Factorial of " ++ show num ++ " using recursion: " ++ show resultRec

putStrLn $ "Factorial of " ++ show num ++ " using foldr: " ++ show resultFoldr

Output:

For num = 5, the output will be:

Factorial of 5 using recursion: 120

Factorial of 5 using foldr: 120

Comparison of the Approaches:

1.​ Recursion:​

○​ Concept: This is a direct implementation of the factorial function based on the


mathematical definition.
○​ Code Structure: The recursive approach uses an explicit base case and
recursive step to solve the problem.
○​ Performance: Recursion is a natural way to break down the problem, but
Haskell’s lazy evaluation may lead to stack overflow if the recursion depth is
too large. However, Haskell optimizes tail-recursive functions (if written
correctly) to avoid deep recursion stacks.
2.​ Higher-Order Function (foldr):​

○​ Concept: Using foldr, we can abstract the accumulation process, making


the code concise and functional. This approach emphasizes the use of
functional programming paradigms (e.g., map/reduce).
○​ Code Structure: This approach avoids manual recursion and instead relies on
a higher-order function to iterate over a list.
○​ Performance: The performance is similar to recursion for small numbers, but
like recursion, this implementation may also suffer from inefficiency or stack
overflow for very large numbers due to the depth of the list.

Q26. Explain the importance of higher-order functions in Haskell. Provide examples of


foldr and foldl to illustrate their functionality.

A. Importance of Higher-Order Functions in Haskell

Higher-order functions are one of the key features of Haskell and functional programming in
general. They allow for more abstract, modular, and reusable code. A higher-order function is
one that either:

1.​ Takes one or more functions as arguments.


2.​ Returns a function as a result.

The importance of higher-order functions in Haskell lies in their ability to allow you to treat
functions as first-class citizens. This leads to more powerful and expressive programming
patterns, such as:

●​ Abstraction: Higher-order functions allow you to abstract out common patterns of


computation, such as folding, mapping, and filtering, without having to rewrite the
logic each time.
●​ Composability: Functions can be combined, manipulated, and composed in flexible
ways.
●​ Modularity: Higher-order functions can be used to build small, reusable components
that can be combined to form complex systems.

In Haskell, higher-order functions such as foldr and foldl are commonly used for
reducing or accumulating values over a list or other data structures.

Explanation of foldr and foldl

Both foldr (fold right) and foldl (fold left) are higher-order functions that process a list
by combining its elements using a binary function. They both reduce a list of values to a
single value (often referred to as "accumulation" or "reduction"), but they differ in the
direction in which they process the list.
1.​ foldr (Fold Right):​

○​ The foldr function processes the list from right to left. It takes a function, a
starting accumulator value, and a list. It applies the function to the elements of
the list, combining the elements starting from the rightmost element.

The signature of foldr is:​


foldr :: (a -> b -> b) -> b -> [a] -> b

○​
○​ foldr f z [x1, x2, ..., xn] is equivalent to f x1 (f x2 (
... (f xn z) ... )).
2.​ foldl (Fold Left):​

○​ The foldl function processes the list from left to right. It also takes a
function, an accumulator, and a list, but it applies the function to the elements
starting from the leftmost element.

The signature of foldl is:​


foldl :: (b -> a -> b) -> b -> [a] -> b

○​
○​ foldl f z [x1, x2, ..., xn] is equivalent to f (...(f (f z
x1) x2) ...) xn.

Examples of foldr and foldl

Let's look at examples to better understand how both foldr and foldl work.

Example 1: Using foldr to Sum a List

-- Using foldr to sum a list

sumList :: [Int] -> Int

sumList = foldr (+) 0

-- Test the function

main :: IO ()

main = do
let result = sumList [1, 2, 3, 4, 5]

print result -- Output will be 15

Explanation of foldr in this case:

foldr (+) 0 [1, 2, 3, 4, 5] works as:​


1 + (2 + (3 + (4 + (5 + 0))))) = 15

●​
●​ It starts by applying + from the rightmost element (5) and continues towards the
leftmost element (1), accumulating the sum.

Example 2: Using foldl to Sum a List

-- Using foldl to sum a list

sumListLeft :: [Int] -> Int

sumListLeft = foldl (+) 0

-- Test the function

main :: IO ()

main = do

let result = sumListLeft [1, 2, 3, 4, 5]

print result -- Output will be 15

Explanation of foldl in this case:

foldl (+) 0 [1, 2, 3, 4, 5] works as:​


(((0 + 1) + 2) + 3) + 4) + 5 = 15

●​
●​ It starts by applying + from the leftmost element (1) and continues towards the
rightmost element (5), accumulating the sum.

○​ .

Example 3: Difference Between foldr and foldl with a Non-Associative Function


Let’s look at a case where the function used in the fold is non-associative. For example,
subtraction (-) is non-associative:

-- Using foldr with subtraction

foldrSubtract :: [Int] -> Int

foldrSubtract = foldr (-) 0

-- Using foldl with subtraction

foldlSubtract :: [Int] -> Int

foldlSubtract = foldl (-) 0

main :: IO ()

main = do

let resultR = foldrSubtract [1, 2, 3, 4, 5]

let resultL = foldlSubtract [1, 2, 3, 4, 5]

print resultR -- Output: -3

print resultL -- Output: -13

Q27. Explain the basic data types in LISP. Provide examples to demonstrate the usage
of numbers, strings, and symbols in a LISP program.

A. Basic Data Types in LISP:

LISP (LISt Processing) is one of the oldest programming languages and is known for its
symbolic expression (S-expression) structure. In LISP, everything is represented as a list or
an atom, with atoms being the basic building blocks of data. The core data types in LISP
include:

1.​ Numbers: LISP supports both integer and floating-point numbers.


2.​ Strings: Sequences of characters enclosed in double quotes.
3.​ Symbols: Named identifiers that represent variables, functions, or other objects. They
are like identifiers in most other languages but have a special meaning in LISP.
4.​ Lists: The primary data structure in LISP, consisting of atoms or other lists.

Example Usage of Numbers, Strings, and Symbols

Below is a LISP program that demonstrates the usage of numbers, strings, and symbols.

Example LISP Program


;; Numbers: Demonstrating integers and floating-point numbers

(setq num1 42) ; Assign an integer to num1

(setq num2 3.14) ; Assign a floating-point number to num2

;; Performing arithmetic with numbers

(setq sum (+ num1 num2)) ; Sum of num1 and num2

(setq product (* num1 num2)) ; Product of num1 and num2

;; Strings: Demonstrating string usage

(setq greeting "Hello, LISP!") ; Assign a string to the variable greeting

;; Using string functions (e.g., concatenation)

(setq message (concatenate 'string greeting " Welcome to the world of LISP."))

;; Symbols: Demonstrating symbol usage

(setq x 10) ; x is a symbol and holds the value 10

(setq y 20) ; y is a symbol and holds the value 20

;; Symbolic operation: adding the values of symbols x and y

(setq result (+ x y)) ; Adding the values of x and y using the symbols

Output of the Program (in the LISP environment):

; After running the code, you can inspect the results:

print(sum) ; 45.14 (Sum of 42 and 3.14)

print(product) ; 131.88 (Product of 42 and 3.14)

print(message) ; "Hello, LISP! Welcome to the world of LISP."

print(result) ; 30 (Sum of 10 and 20)

Q28. Evaluate the advantages of using symbols in LISP for representing


constants.Discuss how symbols enhance code readability and modularity in large LISP
programs.

A. Advantages of Using Symbols in LISP for Representing Constants:


In LISP, symbols play a crucial role in representing constants, variables, and other entities
within a program. Using symbols for constants offers several advantages, especially in terms
of code readability, modularity, and maintaining large codebases.

Summary of Advantages:

● Readability: Symbols make the code more expressive and self-documenting.

● Modularity: Constants represented by symbols are easily reusable and maintainable across
the program

.● Maintainability: Changes to constant values only need to be made in one place, ensuring
consistency and reducing the risk of errors

.● Symbolic Computation: Symbols enable symbolic manipulation, which is a powerful


feature in LISP.

● Consistency: Using symbols ensures that constants are referenced consistently throughout
the code.

● Dynamic Programming: LISP allows symbols to be dynamically evaluated and updated,


providing flexibility

.● Memory Efficiency: Symbols are stored once in a symbol table, reducing memory usage.

1. Improved Code Readability

Symbols in LISP help make code more expressive and easier to read by providing meaningful
names for constants instead of using arbitrary values. When you use a symbol, the meaning
of the constant becomes clear, which improves the self-documenting nature of the code.

Example: Without symbols, constants like 3.14159 would appear in the code multiple
times. If you used a symbol like PI instead, it would be clear that the value represents the
mathematical constant for Pi.​

;; Without symbol: The meaning of '3.14159' is unclear without context.

(setq area (* 3.14159 radius radius))

;; With symbol: 'PI' clearly represents the constant value for Pi.

(setq PI 3.14159)

(setq area (* PI radius radius))


●​ In the second example, using PI makes it immediately clear what the value
represents, improving readability. It also provides a single place to update the value of
the constant if needed, reducing the risk of errors.

2. Modularity

Symbols enable greater modularity by decoupling the constant’s value from its usage in the
code. Instead of hardcoding values throughout the program, you can define constants once
using symbols and refer to them wherever needed. This modular approach allows easier
updates to constants and enhances the reusability of code.

Example: If PI is used throughout the program, you only need to change its definition in one
place, rather than updating every occurrence of 3.14159 manually.​

;; Define the constant once

(setq PI 3.14159)

;; Reuse the constant symbol in calculations

(setq area (* PI radius radius))

(setq circumference (* PI diameter))

●​ If the value of Pi ever changes or needs to be adjusted for precision, you can simply
update the definition of PI, and the rest of the code will automatically use the new
value.

Q29. Design a LISP program to define a list of student marks and calculate the average
mark.

A. LISP Program to Calculate the Average of Student Marks:

LISP Code:

;; Step 1: Define a list of student marks

(setq student-marks '(85 90 78 92 88 75 96))

;; Step 2: Calculate the sum of the marks

(setq total-marks (apply '+ student-marks))


;; Step 3: Calculate the number of students

(setq num-students (length student-marks))

;; Step 4: Calculate the average mark

(setq average-mark (/ total-marks num-students))

;; Step 5: Print the result

(format t "The average mark is: ~f~%" average-mark)

Output:

The average mark is: 85.142857

B. Demonstrate the use of basic list operations such as car, cdr and cons.

A. Basic List Operations in LISP: car, cdr, and cons

1.​ car: Returns the first element (head) of a list.


2.​ cdr: Returns the rest of the list (tail), excluding the first element.
3.​ cons: Constructs a new list by adding an element to the front of an existing list.

Let's demonstrate them with examples.

Example LISP Code:

;; Define a simple list

(setq my-list '(10 20 30 40 50))

;; 1. Using car

(setq first-element (car my-list))

(format t "First element using car: ~A~%" first-element) ;

2. Using cdr

: (setq tail-list (cdr my-list))


(format t "Tail of the list using cdr: ~A~%" tail-list) ;

3. Using cons:

(setq new-list (cons 5 my-list))

(format t "New list using cons: ~A~%" new-list) ; ​

Output of the Program:

First element using car: 10

Tail of the list using cdr: (20 30 40 50)

New list using cons: (5 10 20 30 40 50)

Q30. Create a LISP program to generate the Fibonacci series up to a given number
using both recursion and iteration. Compare the performance of these approaches.

A. LISP Program to Generate Fibonacci Series:

1. Recursive Approach to Generate Fibonacci Series

Recursive Fibonacci Program:

(defun fibonacci-recursive (n)

"Returns the nth Fibonacci number using recursion."

(if (or (= n 0) (= n 1)) ; Base case: F(0) = 0 and F(1) = 1

(+ (fibonacci-recursive (- n 1)) (fibonacci-recursive (- n 2)))))

(defun generate-fibonacci-recursive (n)

"Generates the Fibonacci series up to the nth number using recursion."

(mapcar #'fibonacci-recursive (loop for i from 0 to n collect i)))

;; Example usage

(setq n 10)

(format t "Fibonacci series up to ~D (recursive): ~A~%" n (generate-fibonacci-recursive n))


2. Iterative Approach to Generate Fibonacci Series

Iterative Fibonacci Program:

(defun fibonacci-iterative (n)

"Returns the nth Fibonacci number using iteration."

(let ((a 0) (b 1) (temp 0))

(if (= n 0) a

(loop for i from 2 to n do

(setq temp (+ a b)) ; Sum of the previous two Fibonacci numbers

(setq a b) ; Move 'b' to 'a'

(setq b temp)) ; Set 'b' to the new value

b)))

(defun generate-fibonacci-iterative (n)

"Generates the Fibonacci series up to the nth number using iteration."

(loop for i from 0 to n collect (fibonacci-iterative i)))

;; Example usage

(setq n 10)

(format t "Fibonacci series up to ~D (iterative): ~A~%" n (generate-fibonacci-iterative n))

Example Output:

If we set n = 10 for both approaches:

Fibonacci series up to 10 (recursive): (0 1 1 2 3 5 8 13 21 34 55)

Fibonacci series up to 10 (iterative): (0 1 1 2 3 5 8 13 21 34 55)

Comparing Performance:

Recursive Approach:
The recursive approach has exponential time complexity (O(2^n)) . This makes the recursive
approach inefficient for large values of n.

As the number grows, the number of function calls increases rapidly, leading to significant
performance degradation.

Iterative Approach:

The iterative approach has linear time complexity (O(n))This makes it much more efficient
for large values of n.

It avoids the overhead of recursion and repeated calculations, making it the preferred
approach for performance.

You might also like