11th Week Python Experience

11th Week Python Experience

Photo by Max Duzij on Unsplash

Welcome to our blog where we explore two crucial aspects of Python programming: exception handling and functional programming. Python's simplicity and versatility make it a favorite among developers worldwide. In this journey, we'll master the art of handling exceptions gracefully and dive into the elegance of functional programming. Whether you're a seasoned Pythonista or a newcomer, join us as we uncover the power and beauty of Python's capabilities.

Exception handling

We use exception handling to print out meaningful error messages that may help someone encountering them solve them. It is considered as an industry standard.

  1. try: command first tries if the code under it can run without error.

  2. If it doesn’t, the except: command runs the code under it.

This way we can handle NameError. We can stack up multiple except commands

This way we can handle the FileNotFoundError .

The last except block is for unforeseen errors apart from the above 3 errors that may arrive.

The finally block is always executed before termination of the program.

user defined exception :

We can create our own exception warnings using raise Exception command

def divide_numbers(x, y):
    try:
        result = x / y
    except ZeroDivisionError:
        print("Error: Cannot divide by zero!")
        result = None
    finally:
        print("Division attempt completed.")
    return result

def validate_age(age):
    try:
        if age < 0:
            raise ValueError("Age cannot be negative.")
        elif age < 18:
            raise ValueError("You must be 18 or older.")
    except ValueError as ve:
        print("Error:", ve)

def main():
    # Example 1: Division
    print("--- Example 1: Division ---")
    result = divide_numbers(10, 2)
    print("Result:", result)
    result = divide_numbers(10, 0)
    print("Result:", result)

    # Example 2: Age validation
    print("\n--- Example 2: Age Validation ---")
    validate_age(25)
    validate_age(-5)
    validate_age(16)

if __name__ == "__main__":
    main()

Functional Programming

Functional programming is like cooking with recipes.

  1. Functions are like recipes: In functional programming, we use functions a lot, just like you follow recipes to cook a meal. Each function does one specific task, like chopping vegetables or mixing ingredients.

  2. No changing ingredients: In functional programming, we treat data like ingredients in a recipe. Once we have them, we don't change them directly. Instead, we create new data by mixing or transforming the existing data, just like you create a new dish from different ingredients.

  3. Keep it clean and organized: Functional programming encourages us to keep our code clean and organized, just like a tidy kitchen. We aim to write functions that do one thing well and don't have side effects, like making a mess in the kitchen.

  4. Reuse and share: Functional programming emphasizes reusing code and sharing functions, just like sharing recipes with friends. Instead of rewriting the same steps over and over, we can use existing functions to solve similar problems.

  5. Follow the order: In functional programming, we follow a clear order of operations, just like following the steps in a recipe. Each function takes input, does its job, and produces an output, which can then be used as input for the next function.

In essence, functional programming is about using functions to transform data in a clean, organized, and reusable way, much like following recipes to cook a delicious meal.

we will be discussing about Iterator, Generator, Inline statements, List comprehension, Lambda function , Enumerator , Zip, Map, Filter :::

  1. Iterator: An iterator is an object that allows you to traverse through all the elements of a collection, like lists, tuples, or dictionaries, without needing to know the internal structure of the collection. It provides methods like iter() and next() to access elements sequentially.

    Example:

     my_list = [1, 2, 3, 4, 5]
     iter_obj = iter(my_list)
     print(next(iter_obj))  # Output: 1
     print(next(iter_obj))  # Output: 2
    
  2. Generator: A generator is a special type of iterator that generates values on the fly using the yield keyword. It allows you to iterate over a potentially infinite sequence without storing the entire sequence in memory. A generator is like a factory that produces values one at a time. Unlike a regular function that returns a single value and exits, a generator can pause its execution and yield a value, then resume where it left off when asked to produce the next value.

    Example:

     my_generator():
         yield 1
         yield 2
         yield 3
    
     gen = my_generator()
     print(next(gen))  # Output: 1
     print(next(gen))  # Output: 2
    

    How does it work?

    When you define a generator function, you use the yield keyword instead of return to provide a value. This tells Python that the function is a generator.

      • When you call a generator function, it returns a generator object. This object can be used to iterate over the values produced by the generator function.

        • Each time you call the next() function on the generator object, Python executes the generator function until it encounters a yield statement. It then yields the value provided by the yield statement and pauses execution.

        • When you call next() again, Python resumes execution of the generator function from where it left off, continuing until the next yield statement or the end of the function.

Example with Explanation:

     square(limit):
        x = 1
        while x <= limit:
            yield x * x
            yield x * x * x
            x += 1

    # Create a generator object
    a = square(5)

    print(next(a))  # Output: 1 (square of 1)
    print(next(a))  # Output: 1 (cube of 1)
    print(next(a))  # Output: 4 (square of 2)
    print(next(a))  # Output: 8 (cube of 2)
    print(next(a))  # Output: 9 (square of 3)
    print(next(a))  # Output: 27 (cube of 3)
    print(next(a))  # Output: 16 (square of 4)
    print(next(a))  # Output: 64 (cube of 4)
    print(next(a))  # Output: 25 (square of 5)
    print(next(a))  # Output: 125 (cube of 5)
    8

Lets see what happens for first 3 print statements ?

Let's break down why we need two calls to next(a) to iterate from 1 to 2:

  1. Firstnext(a)call:

    • When we make the first call to next(a), it advances the generator to the first yield statement inside the square() generator function.

    • Inside the generator function, x is initially 1. So, the first yield statement yields the square of 1, which is 1*1 = 1.

    • The value 1 is returned by the first next(a) call, which is why we get the output: 1 (square of 1).

  2. Secondnext(a)call:

    • When we make the second call to next(a), it resumes execution of the generator from where it left off, after the first yield statement.

    • Since the generator paused after the first yield statement, x is still 1.

    • The next yield statement in the generator function yields the cube of 1, which is 111 = 1.

    • The value 1 is returned by the second next(a) call, which is why we get the output: 1 (cube of 1).

  3. Thirdnext(a)call:

    • When we make the third call to next(a), it advances the generator to the second iteration of the loop inside the square() generator function.

    • Now, x becomes 2.

    • The first yield statement inside the loop yields the square of 2, which is 2*2 = 4.

    • The value 4 is returned by the third next(a) call, which is why we get the output: 4 (square of 2).

In the given square() generator function, it yields the square and cube of each number from 1 up to and including the limit.

However, the issue lies in the usage of the next() function. When you call next(a), it advances the generator object a to the next yield statement and returns the value.

  1. Inline statements: Inline statements are concise expressions used for simple operations. They are often used within other constructs like list comprehensions or lambda functions.

    Example:

     # Using inline if-else statement
     x = 10
     y = "Even" if x % 2 == 0 else "Odd"
     print(y)  # Output: Even
     ###################################################
     a=10
     b=20
     """if a<b:
       small=a
     else:
       small=b"""
    
     small= a if a<b else b #inline statement 
    
     ###########################################
     c=5
     """
     while(c<10):
       print(c)
       c+=1 """
     while(c<10): print(c) ;c+=1
     #################################################
    
     numbers = [1, 2, 3, 4, 5]
     # Iterate over each number in the list using a for loop
    
     for num in numbers: print(num ** 2, end=" ");print("@")
     #OP: 1 @
     #    2 @ 
     #    3 @
     #    4 @
     #    5 @
    
     ########################################################
    
  2. List comprehension: List comprehension is a concise way to create lists in Python by iterating over an iterable and applying an expression to each element.

    Example:

     squares = [x**2 for x in range(1, 6)]
     print(squares)  # Output: [1, 4, 9, 16, 25]
     ###################################################
     fruits=["mango","banana","apple", "orange", "pineapple", "watermelon", "guava", "kiwi"]
     """
     newlist=[]
     for fruit in fruits :
       if "n" in fruit:
         newlist.append(fruit.capitalize())
     """
     newlist = [fruit.capitalize() for fruit in fruits if "n" in fruit
     print(newlist)
     # OP: ['Mango', 'Banana', 'Orange', 'Pineapple', 'Watermelon']
    
  3. Lambda function: Lambda functions, also known as anonymous functions, are small, inline functions defined using the lambda keyword. They are typically used for simple operations.

    Example:

     add = lambda x, y: x + y
     print(add(3, 4))  # Output: 7
    
  4. Enumerator: Enumerator is a built-in Python function that adds a counter to an iterable and returns it as an enumerate object. The enumerate() function in Python is used to iterate over a sequence (such as a list, tuple, or string) while also providing the index of each item in the sequence. It returns an enumerate object, which yields pairs of index and value for each item in the sequence.

    Example:

     fruits = ['apple', 'banana', 'cherry']
     for index, fruit in enumerate(fruits):
         print(index, fruit)
     # Output:
     # 0 apple
     # 1 banana
     # 2 cherry
    
  5. Zip: Zip is a built-in Python function that takes iterables (like lists, tuples) as arguments and returns an iterator(Zip Object) which contains wtuples where the i-th tuple contains the i-th element from each of the input iterables.

    Example:

     list1 = ['a', 'b', 'c']
     list2 = [1, 2, 3]
     zipped = zip(list1, list2)
     print(zipped) # Output:  <zip object at 0x7baa4408db00>
     print(list(zipped))  # Output: [('a', 1), ('b', 2), ('c', 3)]
     print(dict(zipped))
    
  6. Map: The map() function in Python is used to apply a specified function to each item in an iterable (such as a list, tuple, or string) and returns an iterator of the results. It takes two parameters: the function to apply and the iterable to apply it to.

    Here's the syntax of the map() function:

     map(function, iterable)
    
    • function: A function that takes one argument. This function will be applied to each item in the iterable.

    • iterable: An iterable (like a list, tuple, or string) whose items will be passed to the function one by one.

The map() function applies the specified function to each item in the iterable sequentially, and it returns an iterator that yields the results. It's important to note that the map() function does not modify the original iterable; it creates and returns a new iterator containing the results of applying the function to each item.

Example:

    a=[10,20,30,40,50,60,70,80,90,100]
    b=[20,30,40,50,60,70,80,90,100,110]

    def sub(x,y):
      return x-y

    c=map(sub,a,b)
    print(c) #output: map object at 0x7f7f7e8e5b50
    print(list(c)) #output: [-10,-10,-10,-10,-10,-10,-10,-10,-10,-10]

    #################################################################

    numbers = [1, 2, 3, 4, 5]
    squared = map(lambda x: x**2, numbers)
    print(list(squared))  # Output: [1, 4, 9, 16, 25]
    ################################################################
    # Define a function to square a number
    def square(x):
        return x ** 2

    # Apply the square function to each item in a list using map
    numbers = [1, 2, 3, 4, 5]
    squared_numbers = map(square, numbers)

    # Convert the result to a list for easier visualization
    squared_numbers_list = list(squared_numbers)

    print(squared_numbers_list)
  1. Filter: Filter is a built-in Python function that constructs an iterator from elements of an iterable for which a function returns true.

    Example:

     numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
     even_numbers = filter(lambda x: x % 2 == 0, numbers)
     print(list(even_numbers))  # Output: [2, 4, 6, 8]
    

    Filtering even numbers :

    • filter() function takes two arguments:

      • A function that returns either True or False.

      • An iterable to filter through.

    • In this case, a lambda function lambda x: x % 2 == 0 is used. This lambda function takes one argument x and returns True if x is even (i.e., divisible by 2), otherwise returns False.

    • The filter() function then applies this lambda function to each element in the numbers list.

    • It constructs an iterator that yields only those elements for which the lambda function returns True.

Now, let's combine all these concepts into a single Python program:

# Generator function to generate squares of numbers
def square_generator(n):
    for i in range(1, n+1):
        yield i**2

# List comprehension to create a list of cubes of even numbers
cubes_of_even = [x**3 for x in range(1, 11) if x % 2 == 0]

# Lambda function to calculate the area of a circle
circle_area = lambda radius: 3.14 * radius**2

# Zip function to combine two lists
list1 = ['a', 'b', 'c']
list2 = [1, 2, 3]
zipped = zip(list1, list2)

# Map function to calculate the squares of numbers
numbers = [1, 2, 3, 4, 5]
squared = map(lambda x: x**2, numbers)

# Filter function to filter even numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
even_numbers = filter(lambda x: x % 2 == 0, numbers)

# Output
print("Generated Squares:", list(square_generator(5)))
print("List of Cubes of Even Numbers:", cubes_of_even)
print("Area of Circle with Radius 5:", circle_area(5))
print("Zipped List:", list(zipped))
print("Squares of Numbers:", list(squared))
print("Even Numbers:", list(even_numbers))

Output:

Generated Squares: [1, 4, 9, 16, 25]
List of Cubes of Even Numbers: [8, 64, 216, 512, 1000]
Area of Circle with Radius 5: 78.5
Zipped List: [('a', 1), ('b', 2), ('c', 3)]
Squares of Numbers: [1, 4, 9, 16, 25]
Even Numbers: [2, 4, 6, 8]

Thank you for embarking on this exciting journey through Python's intricacies with us! From mastering exception handling to embracing the elegance of functional programming, we've equipped ourselves with essential tools for coding success. But our adventure doesn't end here! Stay tuned for more insightful content, tutorials, and tips to level up your Python skills. Subscribe to our blog for the latest updates and exclusive content, and let's continue our journey to become Python masters together! Happy coding!