Session-3-2-Examples

This page shows the source code for Session-3-2-Examples.py in browser-friendly HTML format. It was generated automatically from the original Python file.

Source File Session-3-2-Examples.py
Folder Chapter-3-Advanced-Sessions
"""
Advanced Python - Session 2: Advanced Functions & Iterators
Code Examples and Projects
"""

from functools import wraps, reduce
from contextlib import contextmanager
import time
from datetime import datetime
import csv

# ============================================
# PART 1: Lambda Functions & Functional Programming
# ============================================
print("=" * 60)
print("PART 1: Lambda Functions & Functional Programming")
print("=" * 60)

# Example 1: Basic Lambda
print("\n--- Example 1: Lambda Functions ---")
square = lambda x: x ** 2
add = lambda x, y: x + y

print(f"Square of 5: {square(5)}")
print(f"Add 3 + 4: {add(3, 4)}")

# Example 2: Lambda with sorting
print("\n--- Example 2: Lambda for Sorting ---")
students = [
    ("Alice", 85),
    ("Bob", 92),
    ("Charlie", 78),
    ("Diana", 95)
]

# Sort by name
sorted_by_name = sorted(students, key=lambda x: x[0])
print(f"Sorted by name: {sorted_by_name}")

# Sort by grade (descending)
sorted_by_grade = sorted(students, key=lambda x: x[1], reverse=True)
print(f"Sorted by grade: {sorted_by_grade}")

# Example 3: Map function
print("\n--- Example 3: Map Function ---")
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(f"Numbers: {numbers}")
print(f"Squared: {squared}")

# Convert strings to uppercase
names = ["alice", "bob", "charlie"]
upper_names = list(map(str.upper, names))
print(f"Names: {names}")
print(f"Uppercase: {upper_names}")

# Example 4: Filter function
print("\n--- Example 4: Filter Function ---")
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = list(filter(lambda x: x % 2 == 0, numbers))
odds = list(filter(lambda x: x % 2 != 0, numbers))
print(f"Numbers: {numbers}")
print(f"Even numbers: {evens}")
print(f"Odd numbers: {odds}")

# Example 5: Reduce function
print("\n--- Example 5: Reduce Function ---")
numbers = [1, 2, 3, 4, 5]
sum_all = reduce(lambda x, y: x + y, numbers)
product = reduce(lambda x, y: x * y, numbers)
print(f"Numbers: {numbers}")
print(f"Sum: {sum_all}")
print(f"Product: {product}")

# Example 6: Combining map, filter, reduce
print("\n--- Example 6: Combining Functional Tools ---")
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Sum of squares of even numbers
result = reduce(
    lambda x, y: x + y,
    map(lambda x: x**2, filter(lambda x: x % 2 == 0, numbers))
)
print(f"Sum of squares of even numbers: {result}")

# ============================================
# PART 2: Closures
# ============================================
print("\n" + "=" * 60)
print("PART 2: Closures")
print("=" * 60)

# Example 1: Basic closure
print("\n--- Example 1: Basic Closure ---")
def make_multiplier(n):
    """Creates a function that multiplies by n"""
    def multiplier(x):
        return x * n  # 'n' is captured from outer scope
    return multiplier

times_3 = make_multiplier(3)
times_5 = make_multiplier(5)

print(f"times_3(10) = {times_3(10)}")
print(f"times_5(10) = {times_5(10)}")

# Example 2: Logger with closure
print("\n--- Example 2: Logger Closure ---")
def make_logger(prefix):
    """Creates a logger with a specific prefix"""
    def log(message):
        timestamp = datetime.now().strftime("%H:%M:%S")
        print(f"[{timestamp}] [{prefix}] {message}")
    return log

error_log = make_logger("ERROR")
info_log = make_logger("INFO")
debug_log = make_logger("DEBUG")

error_log("File not found")
info_log("Process started")
debug_log("Variable x = 42")

# Example 3: Counter with closure
print("\n--- Example 3: Counter Closure ---")
def make_counter():
    """Creates a counter function"""
    count = 0
    
    def counter():
        nonlocal count  # Modify outer variable
        count += 1
        return count
    
    return counter

counter1 = make_counter()
counter2 = make_counter()

print(f"Counter1: {counter1()}, {counter1()}, {counter1()}")
print(f"Counter2: {counter2()}, {counter2()}")

# Example 4: HTML tag generator
print("\n--- Example 4: HTML Tag Generator ---")
def make_tag(tag):
    """Creates function to wrap content in HTML tag"""
    def wrap_content(content):
        return f"<{tag}>{content}</{tag}>"
    return wrap_content

h1 = make_tag("h1")
p = make_tag("p")
div = make_tag("div")

print(h1("Welcome to Python"))
print(p("This is a paragraph"))
print(div(p("Nested content")))

# ============================================
# PART 3: Decorators
# ============================================
print("\n" + "=" * 60)
print("PART 3: Decorators")
print("=" * 60)

# Example 1: Basic decorator
print("\n--- Example 1: Basic Decorator ---")
def simple_decorator(func):
    @wraps(func)
    def wrapper():
        print("Before function")
        func()
        print("After function")
    return wrapper

@simple_decorator
def say_hello():
    print("Hello!")

say_hello()

# Example 2: Decorator with arguments
print("\n--- Example 2: Decorator with Arguments ---")
def repeat(times):
    """Decorator that repeats function execution"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Hello {name}!")

greet("Alice")

# Example 3: Timer decorator
print("\n--- Example 3: Timer Decorator ---")
def timer(func):
    """Measures function execution time"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    """Simulate slow operation"""
    time.sleep(0.5)
    return "Done"

result = slow_function()

# Example 4: Logging decorator
print("\n--- Example 4: Logging Decorator ---")
def log_calls(func):
    """Logs function calls and returns"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

result = add(5, 3)

# Example 5: Validation decorator
print("\n--- Example 5: Validation Decorator ---")
def validate_positive(func):
    """Ensures all arguments are positive"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        for arg in args:
            if isinstance(arg, (int, float)) and arg < 0:
                raise ValueError("All arguments must be positive")
        return func(*args, **kwargs)
    return wrapper

@validate_positive
def calculate_area(width, height):
    return width * height

try:
    print(f"Area: {calculate_area(5, 10)}")
    print(f"Area: {calculate_area(-5, 10)}")  # Will raise error
except ValueError as e:
    print(f"Error: {e}")

# Example 6: Chaining decorators
print("\n--- Example 6: Chaining Decorators ---")
@timer
@log_calls
def multiply(a, b):
    time.sleep(0.2)  # Simulate work
    return a * b

result = multiply(4, 5)

# Example 7: Class decorator
print("\n--- Example 7: Class Decorator ---")
def singleton(cls):
    """Ensures only one instance of class exists"""
    instances = {}
    
    @wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance

@singleton
class Database:
    def __init__(self):
        print("Creating Database instance")

db1 = Database()
db2 = Database()  # Returns same instance
print(f"db1 is db2: {db1 is db2}")

# ============================================
# PART 4: Generators
# ============================================
print("\n" + "=" * 60)
print("PART 4: Generators")
print("=" * 60)

# Example 1: Basic generator
print("\n--- Example 1: Basic Generator ---")
def countdown(n):
    """Generator that counts down from n"""
    while n > 0:
        yield n
        n -= 1

for num in countdown(5):
    print(num, end=" ")
print()

# Example 2: Infinite generator
print("\n--- Example 2: Infinite Generator ---")
def infinite_sequence():
    """Generates infinite sequence of numbers"""
    num = 0
    while True:
        yield num
        num += 1

# Take first 10 numbers
gen = infinite_sequence()
first_10 = [next(gen) for _ in range(10)]
print(f"First 10 numbers: {first_10}")

# Example 3: Fibonacci generator
print("\n--- Example 3: Fibonacci Generator ---")
def fibonacci(n):
    """Generates first n Fibonacci numbers"""
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

fibs = list(fibonacci(10))
print(f"First 10 Fibonacci numbers: {fibs}")

# Example 4: Generator expression
print("\n--- Example 4: Generator Expression ---")
# List comprehension (creates entire list)
squares_list = [x**2 for x in range(10)]
print(f"List: {squares_list}")

# Generator expression (lazy evaluation)
squares_gen = (x**2 for x in range(10))
print(f"Generator: {squares_gen}")
print(f"Values: {list(squares_gen)}")

# Example 5: File reading generator
print("\n--- Example 5: File Reading Generator ---")
def read_file_lines(filename):
    """Generator to read file line by line"""
    try:
        with open(filename, 'r') as file:
            for line in file:
                yield line.strip()
    except FileNotFoundError:
        print(f"File {filename} not found")
        return

# Create sample file
with open('sample.txt', 'w') as f:
    f.write("Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n")

print("Reading file with generator:")
for line in read_file_lines('sample.txt'):
    print(f"  {line}")

# Example 6: Generator pipeline
print("\n--- Example 6: Generator Pipeline ---")
def generate_numbers(n):
    """Generate numbers"""
    for i in range(n):
        yield i

def square_numbers(numbers):
    """Square each number"""
    for num in numbers:
        yield num ** 2

def filter_even(numbers):
    """Keep only even numbers"""
    for num in numbers:
        if num % 2 == 0:
            yield num

# Create pipeline
numbers = generate_numbers(10)
squared = square_numbers(numbers)
evens = filter_even(squared)

result = list(evens)
print(f"Pipeline result: {result}")

# ============================================
# PART 5: Iterators
# ============================================
print("\n" + "=" * 60)
print("PART 5: Iterators")
print("=" * 60)

# Example 1: Basic iterator
print("\n--- Example 1: Basic Iterator ---")
class CountDown:
    """Iterator that counts down"""
    
    def __init__(self, start):
        self.current = start
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

counter = CountDown(5)
for num in counter:
    print(num, end=" ")
print()

# Example 2: Range-like iterator
print("\n--- Example 2: Custom Range Iterator ---")
class MyRange:
    """Custom implementation of range()"""
    
    def __init__(self, start, end, step=1):
        self.current = start
        self.end = end
        self.step = step
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if (self.step > 0 and self.current >= self.end) or \
           (self.step < 0 and self.current <= self.end):
            raise StopIteration
        value = self.current
        self.current += self.step
        return value

print("Custom range(0, 10, 2):")
for num in MyRange(0, 10, 2):
    print(num, end=" ")
print()

# Example 3: Data batcher iterator
print("\n--- Example 3: Data Batcher ---")
class DataBatcher:
    """Iterator that yields data in batches"""
    
    def __init__(self, data, batch_size):
        self.data = data
        self.batch_size = batch_size
        self.index = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index >= len(self.data):
            raise StopIteration
        
        batch = self.data[self.index:self.index + self.batch_size]
        self.index += self.batch_size
        return batch

data = list(range(10))
batcher = DataBatcher(data, 3)

print("Batches:")
for batch in batcher:
    print(f"  {batch}")

# Example 4: CSV reader iterator
print("\n--- Example 4: CSV Reader Iterator ---")
class CSVReader:
    """Iterator for reading CSV files"""
    
    def __init__(self, filename):
        self.filename = filename
        self.file = None
        self.reader = None
    
    def __iter__(self):
        self.file = open(self.filename, 'r')
        self.reader = csv.DictReader(self.file)
        return self
    
    def __next__(self):
        try:
            return next(self.reader)
        except StopIteration:
            self.file.close()
            raise

# Create sample CSV
with open('data.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['name', 'age', 'city'])
    writer.writerow(['Alice', '25', 'NYC'])
    writer.writerow(['Bob', '30', 'LA'])
    writer.writerow(['Charlie', '35', 'Chicago'])

print("CSV data:")
for row in CSVReader('data.csv'):
    print(f"  {row}")

# ============================================
# PART 6: Context Managers
# ============================================
print("\n" + "=" * 60)
print("PART 6: Context Managers")
print("=" * 60)

# Example 1: Basic context manager
print("\n--- Example 1: File Context Manager ---")
class FileManager:
    """Custom file context manager"""
    
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None
    
    def __enter__(self):
        print(f"Opening {self.filename}")
        self.file = open(self.filename, self.mode)
        return self.file
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"Closing {self.filename}")
        if self.file:
            self.file.close()
        return False  # Don't suppress exceptions

with FileManager('test.txt', 'w') as f:
    f.write("Hello from context manager!")

# Example 2: Timer context manager
print("\n--- Example 2: Timer Context Manager ---")
class Timer:
    """Context manager for timing code blocks"""
    
    def __init__(self, name="Block"):
        self.name = name
    
    def __enter__(self):
        self.start = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end = time.time()
        self.elapsed = self.end - self.start
        print(f"{self.name} took {self.elapsed:.4f}s")
        return False

with Timer("Loop execution"):
    total = sum(range(1000000))

# Example 3: Using @contextmanager
print("\n--- Example 3: @contextmanager Decorator ---")
@contextmanager
def temporary_change(obj, attr, value):
    """Temporarily change an object's attribute"""
    original = getattr(obj, attr)
    setattr(obj, attr, value)
    try:
        yield
    finally:
        setattr(obj, attr, original)

class Config:
    debug = False

config = Config()
print(f"Debug before: {config.debug}")

with temporary_change(config, 'debug', True):
    print(f"Debug during: {config.debug}")

print(f"Debug after: {config.debug}")

# Example 4: Database connection manager
print("\n--- Example 4: Database Connection Manager ---")
class DatabaseConnection:
    """Simulates database connection context manager"""
    
    def __init__(self, db_name):
        self.db_name = db_name
        self.connection = None
    
    def __enter__(self):
        print(f"Connecting to {self.db_name}...")
        self.connection = f"Connection to {self.db_name}"
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"Closing connection to {self.db_name}")
        self.connection = None
        
        if exc_type is not None:
            print(f"Exception occurred: {exc_val}")
        return False
    
    def execute(self, query):
        print(f"Executing: {query}")
        return f"Result of {query}"

with DatabaseConnection("mydb") as db:
    result = db.execute("SELECT * FROM users")
    print(result)

# ============================================
# PROJECT 1: Advanced Logging System
# ============================================
print("\n" + "=" * 60)
print("PROJECT 1: Advanced Logging System")
print("=" * 60)

class LogLevel:
    DEBUG = 0
    INFO = 1
    WARNING = 2
    ERROR = 3
    CRITICAL = 4

def create_logger(name, level=LogLevel.INFO, filename=None):
    """Factory function to create custom logger"""
    
    def log_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            
            # Before execution
            if level <= LogLevel.INFO:
                message = f"[{timestamp}] [INFO] Calling {func.__name__}"
                print(message)
                if filename:
                    with open(filename, 'a') as f:
                        f.write(message + '\n')
            
            try:
                result = func(*args, **kwargs)
                
                # After successful execution
                if level <= LogLevel.DEBUG:
                    message = f"[{timestamp}] [DEBUG] {func.__name__} returned {result}"
                    print(message)
                    if filename:
                        with open(filename, 'a') as f:
                            f.write(message + '\n')
                
                return result
            
            except Exception as e:
                # On error
                message = f"[{timestamp}] [ERROR] {func.__name__} raised {type(e).__name__}: {e}"
                print(message)
                if filename:
                    with open(filename, 'a') as f:
                        f.write(message + '\n')
                raise
        
        return wrapper
    return log_decorator

# Using the logger
@create_logger("MyApp", level=LogLevel.DEBUG, filename="app.log")
def process_data(data):
    """Process some data"""
    return f"Processed {len(data)} items"

@create_logger("MyApp", level=LogLevel.INFO)
def divide(a, b):
    """Divide two numbers"""
    return a / b

result = process_data([1, 2, 3, 4, 5])
result = divide(10, 2)

try:
    result = divide(10, 0)
except ZeroDivisionError:
    pass

# ============================================
# PROJECT 2: Data Processing Pipeline
# ============================================
print("\n" + "=" * 60)
print("PROJECT 2: Data Processing Pipeline")
print("=" * 60)

def read_csv_generator(filename):
    """Generator to read CSV file"""
    with open(filename, 'r') as file:
        reader = csv.DictReader(file)
        for row in reader:
            yield row

def filter_by_value(rows, field, threshold):
    """Generator to filter rows by field value"""
    for row in rows:
        try:
            if float(row[field]) > threshold:
                yield row
        except (ValueError, KeyError):
            continue

def transform_data(rows, transformations):
    """Generator to transform data"""
    for row in rows:
        new_row = row.copy()
        for field, func in transformations.items():
            if field in new_row:
                new_row[field] = func(new_row[field])
        yield new_row

def batch_processor(rows, batch_size):
    """Generator to process data in batches"""
    batch = []
    for row in rows:
        batch.append(row)
        if len(batch) >= batch_size:
            yield batch
            batch = []
    if batch:  # Yield remaining items
        yield batch

# Create sample data
with open('sales_pipeline.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['product', 'price', 'quantity', 'region'])
    writer.writerow(['Laptop', '999.99', '5', 'East'])
    writer.writerow(['Mouse', '29.99', '50', 'West'])
    writer.writerow(['Keyboard', '79.99', '30', 'East'])
    writer.writerow(['Monitor', '349.99', '15', 'North'])
    writer.writerow(['Desk', '449.99', '8', 'South'])

# Build pipeline
print("\nProcessing pipeline:")
rows = read_csv_generator('sales_pipeline.csv')
filtered = filter_by_value(rows, 'price', 100)
transformed = transform_data(filtered, {
    'price': lambda x: f"${float(x):.2f}",
    'product': str.upper
})

for row in transformed:
    print(f"  {row}")

# ============================================
# PROJECT 3: Advanced Context Manager
# ============================================
print("\n" + "=" * 60)
print("PROJECT 3: Advanced Context Manager")
print("=" * 60)

@contextmanager
def managed_resource(resource_name, setup_time=0.1, cleanup_time=0.1):
    """Generic resource manager with setup and cleanup"""
    print(f"Setting up {resource_name}...")
    time.sleep(setup_time)
    
    resource = {"name": resource_name, "active": True}
    
    try:
        yield resource
    except Exception as e:
        print(f"Error occurred: {e}")
        raise
    finally:
        print(f"Cleaning up {resource_name}...")
        time.sleep(cleanup_time)
        resource["active"] = False

# Using the resource manager
with managed_resource("Database Connection") as db:
    print(f"Using {db['name']}: Active = {db['active']}")
    # Simulate work
    time.sleep(0.2)

print("\n" + "=" * 60)
print("Advanced Session 2 Completed!")
print("Master these patterns for professional Python code!")
print("=" * 60)