Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
1 views

Chapter 2_Python

Uploaded by

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

Chapter 2_Python

Uploaded by

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

Chapter 2

Functions, Modules & Packages, Exceptional Handling

2.1. Function Basics: Scope, Nested Function, Non-local Statements

Scope

- Local Scope: Variables defined inside a function. They are only accessible within
that function.

- Global Scope: Variables defined outside of all functions. They are accessible
anywhere in the code.

- Non-local Scope: Variables in the enclosing scope of a nested function (a function


defined inside another function).

Example:

x = "global"

def outer():

x = "outer"

def inner():

x = "inner" # Local scope of `inner`

print(x)

inner()

print(x) # Outer scope

outer()

print(x) # Global scope


Nested Function

A function defined within another function. It can access variables of the enclosing
function (but not global variables unless specifically declared global).

Example:

def outer_function():

x = "Hello"

def inner_function():

print(x) # Accesses the `x` from the outer function

inner_function()

outer_function()

Non-local Statements

The `nonlocal` keyword allows you to modify variables in the nearest enclosing scope
(but not global scope).

Example:

def outer():

x = "outer"

def inner():

nonlocal x # Refers to the `x` in outer function

x = "inner"

print(x)

inner()

print(x)

outer()
2.2. Built-in Functions

Python provides numerous built-in functions. These are always available for use
without importing any module.

- Common Built-in Functions:

- `print()`: Outputs data to the console.

- `len()`: Returns the length of an object.

- `type()`: Returns the type of an object.

- `max()`, `min()`: Find the maximum or minimum value.

- `sum()`: Returns the sum of all elements in an iterable.

- `sorted()`: Sorts an iterable.

- `input()`: Reads input from the user.

- `map()`, `filter()`: Apply a function to every item in an iterable or filter items based
on a function.

Example:

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

print(sum(numbers)) # Output: 15

print(sorted(numbers, reverse=True)) # Output: [5, 4, 3, 2, 1]

2.3. Types of Functions

User-defined Functions: Functions that are created by the user.

def greet(name):

return f"Hello, {name}"

- Anonymous Functions (Lambda): Functions defined without a name using the


`lambda` keyword. They are typically used for small, simple operations.

A lambda function is a small anonymous function.


Syntax: lambda arguments: expression

The expression is executed and the result is returned.

Lambda function can have any number of arguments but can have only one
expression.

Example of a Lambda Function:

# Lambda function to double a number

double = lambda x: x * 2

print(double(5)) # Output: 10

Lambda functions are often used with functions like `map()`, `filter()`, and `sorted()`.

Example:

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

# Use lambda with map to square each number

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

print(list(squared)) # Output: [1, 4, 9, 16, 25]

Summary:

- Scope refers to the accessibility of variables.

- Nested functions allow functions to be defined inside other functions.

- Non-local allows access to variables from an enclosing scope.

- Python provides numerous built-in functions for common tasks.

- There are different types of functions, including anonymous functions like `lambda`
that are concise and often used for short operations.
2.4. Decorators and Generators

Decorators

A decorator is a function that modifies the behaviour of another function or class.


Decorators are often used to add functionality to existing functions in a clean and
reusable way.

- Syntax: A decorator is applied to a function using the `@decorator_name` syntax.

In python, a decorator is a design pattern that allows you to modify the


functionality of a function by wrapping it in another function.

The outer function is called the decorator, which takes the original function as an
argument and returns a modified version of it.

Example of a Simple Decorator:

def make_pretty(func):

# define the inner function

def inner():

# add some additional behaviour to decorated function

print("I got decorated")

# call original function

func()

# return the inner function

return inner

# define ordinary function

def ordinary():

print("I am ordinary")

# decorate the ordinary function

decorated_func = make_pretty(ordinary)
# call the decorated function

decorated_func()

Output

I got decorated

I am ordinary

In the example shown above, make_pretty() is a decorator.

decorated_func = make_pretty(ordinary)

We are now passing the ordinary() function as the argument to the make_pretty().

The make_pretty() function returns the inner function, and it is now assigned to the
decorated_func variable.

decorated_func()

Here, we are actually calling the inner() function, where we are printing

@ Symbol With Decorator

Instead of assigning the function call to a variable, Python provides a much more
elegant way to achieve this functionality using the @ symbol. For example,

def make_pretty(func):

def inner():

print("I got decorated")

func()

return inner

@make_pretty # ordinary = make_pretty(ordinary).

def ordinary():

print("I am ordinary")

ordinary()
Output

I got decorated

I am ordinary

Here, the ordinary() function is decorated with the make_pretty() decorator using the
@make_pretty syntax, which is equivalent to calling

ordinary = make_pretty(ordinary).

Decorators are commonly used for tasks like logging, authentication, and access
control.

Another Example of a Decorator:

def my_decorator(func):

def wrapper():

print("Before the function runs")

func()

print("After the function runs")

return wrapper

@my_decorator

def say_hello():

print("Hello!")

say_hello()

- Output:

Before the function runs

Hello!

After the function runs


Generator

A Generator in Python is a function that returns an iterator using the Yield


keyword.
In Python, a generator is a function that returns an iterator that produces a
sequence of values when iterated over.
Generators are useful when we want to produce a large sequence of values, but
we don't want to store all of them in memory at once.

Generators are defined using the `yield` keyword.

Create Python Generator


In Python, similar to defining a normal function, we can define a generator
function using the def keyword, but instead of the return statement we use the
yield statement.

If the body of a def contains yield, the function automatically becomes a Python
generator function.

def generator_name(arg):

# statements

yield something

Here, the yield keyword is used to produce a value from the generator.

When the generator function is called, it does not execute the function body
immediately. Instead, it returns a generator object that can be iterated over to
produce the values.
Example: Here's an example of a generator function that produces a sequence
of numbers,

def my_generator(n):

# initialize counter

value = 0

# loop until counter is less than n

while value < n:

# produce the current value of the counter

yield value

# increment the counter

value += 1

# iterate over the generator object produced by my_generator

for value in my_generator(3):

# print each value produced by generator

print(value)

Output

In the above example, the my_generator() generator function takes an integer


n as an argument and produces a sequence of numbers from
0 to n-1 using while loop.
The yield keyword is used to produce a value from the generator and pause the
generator function's execution until the next value is requested.
The for loop iterates over the generator object produced by my_generator() and
the print statement prints each value produced by the generator.

2.5. Modules

Basic Module Usage

A module is a Python file that contains Python code (such as functions, classes, or
variables). It helps organize code into separate files and reuse it in different programs.

- Creating a Module: You create a module by saving Python code in a `.py` file.

Example (file: `mymodule.py`):

def greet(name):

return f"Hello, {name}"

age = 25

Importing Modules

You can import a module using the `import` keyword. Once imported, you can access
the functions and variables defined in the module.

Example:

import mymodule

print(mymodule.greet("Alice")) # Output: Hello, Alice

print(mymodule.age) # Output: 25

You can also give the module a different name using the `as` keyword:

import mymodule as mm

print(mm.greet("Bob")) # Output: Hello, Bob


Types of Imports

1. Import Entire Module:

import math

print(math.sqrt(16)) # Output: 4.0

2. Import Specific Functions or Variables:

You can import specific items from a module using the `from ... import` syntax.

from math import sqrt, pi

print(sqrt(25)) # Output: 5.0

print(pi) # Output: 3.141592653589793

3. Import All Functions/Variables:

You can import everything from a module using `*`. However, this is not
recommended because it can lead to conflicts between names.

from math import *

print(sqrt(36)) # Output: 6.0

Creating and Importing Your Own Modules

To create your own module, just save a Python file with functions and variables you
want to reuse. You can import it in another script using the same syntax as built-in
modules.

1. Step 1: Create a Module File

- Save the following code in a file named `my_module.py`:

def add(a, b):

return a + b
def multiply(a, b):

return a * b

2. Step 2: Import and Use the Module

- In another Python file, you can import and use the functions:

import my_module

print(my_module.add(5, 3)) # Output: 8

print(my_module.multiply(5, 3)) # Output: 15

Importing from Submodules

If you have a package (a directory containing multiple Python files), you can organize
modules into submodules.

Example:

- Directory structure:

mypackage/

__init__.py

module1.py

module2.py

To import functions from `module1.py`:

from mypackage.module1 import my_function


2.6. Importing Functions and Variables from Different Modules

You can import functions or variables from different modules in the same program.

1. Example 1:

from math import sqrt

from random import randint

print(sqrt(49)) # Output: 7.0

print(randint(1, 10)) # Output: Random number between 1 and 10

2. Example 2:

You can import from custom modules.

- Assume you have two files: `math_operations.py` and `string_operations.py`.

- File: `math_operations.py`

def add(a, b):

return a + b

- File: `string_operations.py`

def greet(name):

return f"Hello, {name}!"

- Importing functions from both modules in another script:

from math_operations import add

from string_operations import greet

print(add(10, 5)) # Output: 15

print(greet("Alice")) # Output: Hello, Alice!

This modularity helps in organizing and maintaining your code in an efficient way,
especially for large projects.
2.7. Python Built-in Modules

Python has a wide array of built-in modules that provide various functions to help
with math, random number generation, date/time manipulation, and much more.

1. `math` Module

The `math` module provides mathematical functions.

Common functions:

- `math.sqrt(x)`: Returns the square root of `x`.

- `math.pow(x, y)`: Returns `x` raised to the power of `y`.

- `math.factorial(x)`: Returns the factorial of `x`.

- `math.pi`: Returns the value of pi.

Example:

import math

print(math.sqrt(16)) # Output: 4.0

print(math.pi) # Output: 3.141592653589793

2. `random` Module

The `random` module is used for generating random numbers.

Common functions:

- `random.random()`: Returns a random float between 0 and 1.

- `random.randint(a, b)`: Returns a random integer between `a` and `b`.

- `random.choice(sequence)`: Returns a random element from a sequence (like a list).

- `random.shuffle(list)`: Shuffles the list in place.

Example:

import random
print(random.random()) # Output: Random float between 0 and 1

print(random.randint(1, 10)) # Output: Random integer between 1 and 10

print(random.choice([1, 2, 3])) # Output: Random choice from the list

3. `datetime` Module

The `datetime` module provides classes for manipulating dates and times.

Common functions:

- `datetime.datetime.now()`: Returns the current date and time.

- `datetime.datetime.strptime()`: Parses a string into a `datetime` object.

- `datetime.timedelta()`: Represents the difference between two dates/times.

Example:

import datetime

now = datetime.datetime.now()

print(now) # Output: Current date and time

Other built-in modules include:

- `os`: Interacting with the operating system.

- `sys`: Provides access to system-specific parameters and functions.

- `time`: Provides time-related functions.

2.8. Package: Import Basics


A package is a collection of Python modules organized in directories that allow for
hierarchical structuring of modules. A package typically contains an `__init__.py` file
that makes the directory a Python package.

Creating and Importing a Package

1. Creating a Package:

- Directory structure:

mypackage/

__init__.py

module1.py

module2.py

- `module1.py`:

def hello():

return "Hello from module1!"

- `module2.py`:

def greet(name):

return f"Hello, {name}, from module2!"

2. Importing a Package:

from mypackage import module1, module2

print(module1.hello()) # Output: Hello from module1!

print(module2.greet("Alice")) # Output: Hello, Alice, from module2!

You can also import specific functions or variables from modules in the package:

from mypackage.module1 import hello


2.9. Python Namespace Packages

Namespace packages allow the distribution of a single package across multiple


directories or distributions. This is useful when multiple developers or projects want
to maintain separate sub-packages.

- Namespace Packages do not require an `__init__.py` file in their directories. They


allow different portions of a package to reside in different locations on the filesystem.

Example:

You might have:

- `mypackage/subpackage1/`

- `mypackage/subpackage2/`

Each subpackage can be in different directories, but they can still be imported as part
of the same package.

from mypackage.subpackage1 import module_a

from mypackage.subpackage2 import module_b

Namespace packages make it possible to split large packages across multiple


directories without needing to modify how they are imported.

2.10. User-Defined Modules and Packages

User-Defined Modules

A module is just a Python file (`.py`) containing Python code like functions, variables,
or classes. You can create a user-defined module by writing your Python code in a file
and importing it into other files.
1. Step 1: Create a Module File

- Example (save this code in `my_math.py`):

def add(a, b):

return a + b

def subtract(a, b):

return a - b

2. Step 2: Import and Use the Module

- In another file, you can import and use the functions:

import my_math

print(my_math.add(5, 3)) # Output: 8

print(my_math.subtract(5, 3)) # Output: 2

User-Defined Packages

A package is a collection of user-defined modules organized in a directory.

1. Step 1: Create a Package

- Directory structure:

mypackage/

__init__.py # Optional in modern Python, but used to mark a directory as a


package

my_math.py

my_string.py

- `my_math.py`:

def multiply(a, b):

return a * b
- `my_string.py`:

def uppercase(s):

return s.upper()

2. Step 2: Import and Use the Package

from mypackage import my_math, my_string

print(my_math.multiply(2, 3)) # Output: 6

print(my_string.uppercase("hello")) # Output: HELLO

Using `__init__.py`:

- If you include an `__init__.py` file in the package directory, you can also define the
package's public API or initialization code.

Example `__init__.py`:

from .my_math import multiply

from .my_string import uppercase

Now you can import from the package directly:

import mypackage

print(mypackage.multiply(2, 3)) # Output: 6

print(mypackage.uppercase("hello")) # Output: HELLO

In summary:

- Python modules and packages help organize and reuse code.


- You can create your own modules by writing Python code in `.py` files and
importing them.

- Packages allow you to organize multiple related modules into a single directory
structure.

2.11. Exception Handling

Exception handling in Python allows you to manage runtime errors gracefully,


preventing your program from crashing unexpectedly and providing useful feedback
for debugging. It uses the `try`, `except`, `else`, and `finally` blocks to handle
exceptions effectively.

2.11.1. Avoiding Code Break Using Exception Handling

When an error occurs during the execution of a program, it raises an **exception**. If


the exception is not handled, the program will terminate. Exception handling allows
us to avoid this by catching the error and taking appropriate actions.

Basic Syntax:

try:

# Code that might raise an exception

except SomeException as e:

# Code to handle the exception

Example:

try:

x = 1 / 0 # Division by zero raises an exception

except ZeroDivisionError:

print("You cannot divide by zero!")

Without exception handling, the program would crash. With exception handling, it
shows a user-friendly message.

2.11.2. Safeguarding File Operation Using Exception Handling


File operations can easily fail due to reasons such as missing files, insufficient
permissions, or incorrect file modes. To avoid these issues, you can use exception
handling to safeguard file operations.

Example:

try:

file = open('non_existent_file.txt', 'r')

content = file.read()

except FileNotFoundError:

print("The file does not exist.")

finally:

try:

file.close() # Ensure file is closed if it was opened

except NameError:

pass # File was not opened, so no need to close

In this example, if the file does not exist, the `FileNotFoundError` is caught, and an
appropriate message is shown instead of the program crashing.

2.11.3. Handling Multiple and User-Defined Exceptions

Sometimes, different exceptions may occur within the same block of code. You can
handle multiple exceptions by specifying multiple `except` clauses. Additionally,
Python allows you to define your own exceptions by creating custom exception
classes.

Handling Multiple Exceptions:

try:

x = int(input("Enter a number: "))

result = 10 / x
except ZeroDivisionError:

print("Division by zero is not allowed!")

except ValueError:

print("Invalid input! Please enter a valid number.")

User-Defined Exceptions:

You can create a custom exception by subclassing the built-in `Exception` class.

class CustomError(Exception):

pass

def check_positive(number):

if number <= 0:

raise CustomError("Number must be positive!")

try:

check_positive(-5)

except CustomError as e:

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

2.11.4. Handling and Helping Developer with Error Code

Providing meaningful error codes and messages can help developers understand what
went wrong. When an exception is raised, you can include information about the error
to assist in debugging.

Example with Error Messages:

class InvalidAgeError(Exception):
def __init__(self, age):

self.age = age

super().__init__(f"Invalid age: {age}. Age must be between 0 and 120.")

def validate_age(age):

if age < 0 or age > 120:

raise InvalidAgeError(age)

try:

validate_age(150)

except InvalidAgeError as e:

print(f"Error: {e}")

In this case, the custom `InvalidAgeError` exception includes the invalid age and
provides a clear error message.

2.11.5. Programming Using Exception Handling

Exception handling is critical for writing robust and resilient programs. Here are some
best practices for using exception handling effectively:

1. Use Specific Exceptions: Catch specific exceptions rather than using a general
`except` block. This makes your error handling more precise and avoids masking
unexpected errors.

try:

# Code that might raise an exception

except ValueError:

# Handle ValueError specifically


2. Use `else` for Code Without Exceptions: The `else` block is executed only if no
exceptions occur, allowing you to separate exception-prone code from the rest of the
logic.

try:

result = 10 / 2

except ZeroDivisionError:

print("Cannot divide by zero.")

else:

print("Division successful:", result)

3. Use `finally` for Cleanup: The `finally` block is always executed, regardless of
whether an exception occurred. It’s useful for releasing resources like file handles,
network connections, or databases.

try:

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

content = file.read()

except FileNotFoundError:

print("File not found.")

finally:

file.close() # This will always execute, ensuring the file is closed

4. Avoid Empty `except` Blocks: Avoid using an empty `except` block that catches all
exceptions without handling them, as it can make debugging harder.

try:

# Some risky code

except Exception as e:

print(f"Error occurred: {e}") # Always print or log the error


5. Raise Exceptions When Needed: Don’t hesitate to raise exceptions in your code if
certain conditions are not met. This helps make your program more robust and error-
aware.

def divide(a, b):

if b == 0:

raise ValueError("Cannot divide by zero!")

return a / b

Summary of Exception Handling Techniques:

- Use `try`, `except` blocks to catch and handle exceptions without breaking the
program.

- Safeguard file operations to avoid file access issues.

- Handle multiple exceptions and define custom exceptions for more precise error
handling.

- Provide meaningful error messages and codes to assist developers with debugging.

- Use `finally` to ensure necessary cleanup actions, such as closing files or network
connections.

This structured approach to exception handling helps create resilient and user-friendly
programs.

Lab Assignments:

Lab Assignment 1: Scope and Nested Functions

Problem: Write a Python program demonstrating the use of global, local, and
nonlocal scopes. Also, include a nested function that modifies a variable from the
outer function using the `nonlocal` keyword.

Solution:

# Global scope

x = "global"

def outer():

# Local scope of 'outer'


x = "outer"

def inner():

nonlocal x # Refers to the 'x' in outer function

x = "inner"

print(f"Inner function scope: {x}")

inner()

print(f"Outer function scope: {x}")

outer()

print(f"Global scope: {x}")

Expected Output:

Inner function scope: inner

Outer function scope: inner

Global scope: global

Lab Assignment 2: Built-in Functions

Problem:

Use built-in functions to perform the following tasks:

1. Find the maximum and minimum of a list.

2. Sort a list in reverse order.

3. Use `map()` to square each element in a list.

Solution:

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

# Finding the maximum and minimum values


print(f"Max: {max(numbers)}, Min: {min(numbers)}")

# Sorting in reverse order

print(f"Sorted in reverse: {sorted(numbers, reverse=True)}")

# Using map() to square each number

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

print(f"Squared numbers: {squared_numbers}")

**Expected Output:**

Max: 5, Min: 1

Sorted in reverse: [5, 4, 3, 2, 1]

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

Lab Assignment 3: Decorators

Problem:

Create a decorator that adds functionality to a function by printing "Before" and


"After" the function execution.

Solution:

def my_decorator(func):

def wrapper():

print("Before the function runs")

func()

print("After the function runs")

return wrapper

@my_decorator

def greet():

print("Hello, World!")
greet()

Expected Output:

Before the function runs

Hello, World!

After the function runs

Lab Assignment 4: Generators

Problem:

Write a generator function that yields the first 5 square numbers. Also, use a generator
expression to achieve the same result.

Solution:

# Generator function

def square_numbers():

for i in range(5):

yield i ** 2

gen = square_numbers()

for num in gen:

print(num)

# Generator expression

gen_expr = (x ** 2 for x in range(5))


for num in gen_expr:

print(num)

Expected Output:

16

Lab Assignment 5: Creating and Importing Modules

Problem:

1. Create a Python module named `mymath.py` with two functions `add()` and
`multiply()`.

2. Import this module in another script and use the functions.

Solution:

mymath.py:

def add(a, b):

return a + b

def multiply(a, b):

return a * b

Main Script:

import mymath
print(mymath.add(10, 5)) # Output: 15

print(mymath.multiply(10, 5)) # Output: 50

Expected Output:

15

50

Lab Assignment 6: Exception Handling

Problem:

Write a Python program that:

1. Takes a number as input and raises a `ValueError` if the number is negative.

2. Use `try-except` to handle multiple exceptions for invalid inputs.

Solution:

def check_positive(number):

if number < 0:

raise ValueError("Number must be positive!")

try:

num = int(input("Enter a positive number: "))

check_positive(num)

print(f"Number is: {num}")

except ValueError as e:
print(f"Error: {e}")

Expected Output (for invalid input -5):

Error: Number must be positive!

You might also like