python

solutions

solutions.py🐍
"""
Solutions for Module 10 - Advanced Functions Exercises

These are reference solutions. Try to solve the exercises yourself first!
"""

import functools
import time
from typing import Callable, TypeVar, ParamSpec

P = ParamSpec('P')
T = TypeVar('T')


# =============================================================================
# Decorators - Basic
# =============================================================================

def timer(func: Callable[P, T]) -> Callable[P, T]:
    """Decorator that times function execution."""
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} took {end - start:.4f} seconds")
        return result
    return wrapper


def debug(func: Callable[P, T]) -> Callable[P, T]:
    """Decorator that prints function calls and results."""
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        args_str = ", ".join(repr(a) for a in args)
        kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
        all_args = ", ".join(filter(None, [args_str, kwargs_str]))
        print(f"Calling {func.__name__}({all_args})")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result!r}")
        return result
    return wrapper


def count_calls(func: Callable[P, T]) -> Callable[P, T]:
    """Decorator that counts function calls."""
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper


# =============================================================================
# Decorators - With Arguments
# =============================================================================

def repeat(times: int):
    """Decorator that repeats function execution."""
    def decorator(func: Callable[P, T]) -> Callable[P, list[T]]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> list[T]:
            return [func(*args, **kwargs) for _ in range(times)]
        return wrapper
    return decorator


def retry(max_attempts: int = 3, delay: float = 1.0):
    """Decorator that retries on exception."""
    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            last_error = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_error = e
                    if attempt < max_attempts - 1:
                        time.sleep(delay)
            raise last_error
        return wrapper
    return decorator


def cache_with_ttl(ttl_seconds: float):
    """Decorator that caches results with time-to-live."""
    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        cache = {}
        
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            key = (args, tuple(sorted(kwargs.items())))
            now = time.time()
            
            if key in cache:
                result, timestamp = cache[key]
                if now - timestamp < ttl_seconds:
                    return result
            
            result = func(*args, **kwargs)
            cache[key] = (result, now)
            return result
        
        wrapper.cache_clear = lambda: cache.clear()
        return wrapper
    return decorator


def validate_types(*type_args, **type_kwargs):
    """Decorator that validates argument types."""
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Validate positional arguments
            for i, (arg, expected) in enumerate(zip(args, type_args)):
                if not isinstance(arg, expected):
                    raise TypeError(
                        f"Argument {i} must be {expected.__name__}, "
                        f"got {type(arg).__name__}"
                    )
            
            # Validate keyword arguments
            for key, expected in type_kwargs.items():
                if key in kwargs and not isinstance(kwargs[key], expected):
                    raise TypeError(
                        f"Argument '{key}' must be {expected.__name__}, "
                        f"got {type(kwargs[key]).__name__}"
                    )
            
            return func(*args, **kwargs)
        return wrapper
    return decorator


# =============================================================================
# Closures
# =============================================================================

def make_counter(start: int = 0):
    """Create a counter closure."""
    count = start
    
    def counter():
        nonlocal count
        count += 1
        return count
    
    return counter


def make_accumulator(initial: float = 0):
    """Create an accumulator closure."""
    total = initial
    
    def accumulator(value: float) -> float:
        nonlocal total
        total += value
        return total
    
    return accumulator


def make_averager():
    """Create a running average calculator."""
    values = []
    
    def averager(new_value: float) -> float:
        values.append(new_value)
        return sum(values) / len(values)
    
    return averager


def make_rate_limiter(max_calls: int, period: float):
    """Create a rate limiter closure."""
    calls = []
    
    def rate_limiter() -> bool:
        nonlocal calls
        now = time.time()
        # Remove old calls
        calls = [t for t in calls if now - t < period]
        if len(calls) < max_calls:
            calls.append(now)
            return True
        return False
    
    return rate_limiter


# =============================================================================
# Generators
# =============================================================================

def fibonacci_gen(limit: int = None):
    """Generate Fibonacci numbers."""
    a, b = 0, 1
    count = 0
    while limit is None or count < limit:
        yield a
        a, b = b, a + b
        count += 1


def chunks(iterable, size: int):
    """Yield chunks of specified size."""
    chunk = []
    for item in iterable:
        chunk.append(item)
        if len(chunk) == size:
            yield chunk
            chunk = []
    if chunk:
        yield chunk


def flatten_gen(nested):
    """Flatten nested iterables (generator version)."""
    for item in nested:
        if isinstance(item, (list, tuple)):
            yield from flatten_gen(item)
        else:
            yield item


def sliding_window(iterable, size: int):
    """Generate sliding windows over iterable."""
    from collections import deque
    window = deque(maxlen=size)
    
    for item in iterable:
        window.append(item)
        if len(window) == size:
            yield tuple(window)


def infinite_cycle(iterable):
    """Infinitely cycle through iterable."""
    while True:
        for item in iterable:
            yield item


# =============================================================================
# Generator Expressions vs List Comprehensions
# =============================================================================

def memory_efficient_sum(n: int) -> int:
    """Sum using generator (memory efficient for large n)."""
    return sum(x**2 for x in range(n))


def first_n_primes(n: int):
    """Generate first n prime numbers."""
    def is_prime(num):
        if num < 2:
            return False
        for i in range(2, int(num**0.5) + 1):
            if num % i == 0:
                return False
        return True
    
    count = 0
    num = 2
    while count < n:
        if is_prime(num):
            yield num
            count += 1
        num += 1


# =============================================================================
# Context Managers (Generator-based)
# =============================================================================

from contextlib import contextmanager


@contextmanager
def timer_context():
    """Context manager for timing code blocks."""
    start = time.perf_counter()
    yield
    end = time.perf_counter()
    print(f"Block took {end - start:.4f} seconds")


@contextmanager
def suppress_exceptions(*exceptions):
    """Context manager that suppresses specified exceptions."""
    try:
        yield
    except exceptions:
        pass


@contextmanager
def temporary_change(obj, attr, new_value):
    """Temporarily change an object's attribute."""
    old_value = getattr(obj, attr)
    setattr(obj, attr, new_value)
    try:
        yield
    finally:
        setattr(obj, attr, old_value)


# =============================================================================
# Partial Functions
# =============================================================================

from functools import partial

# Create specialized functions
def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

# Specialized logging
def log(level, message):
    print(f"[{level}] {message}")

info = partial(log, "INFO")
error = partial(log, "ERROR")
debug_log = partial(log, "DEBUG")


# =============================================================================
# Test Solutions
# =============================================================================

if __name__ == "__main__":
    # Test timer decorator
    @timer
    def slow_function():
        time.sleep(0.1)
        return "done"
    
    assert slow_function() == "done"
    
    # Test count_calls
    @count_calls
    def greet(name):
        return f"Hello, {name}"
    
    greet("Alice")
    greet("Bob")
    assert greet.calls == 2
    
    # Test repeat decorator
    @repeat(3)
    def get_random():
        import random
        return random.randint(1, 100)
    
    results = get_random()
    assert len(results) == 3
    
    # Test closures
    counter = make_counter()
    assert counter() == 1
    assert counter() == 2
    assert counter() == 3
    
    acc = make_accumulator(10)
    assert acc(5) == 15
    assert acc(3) == 18
    
    # Test generators
    fib = list(fibonacci_gen(10))
    assert fib == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
    
    chunked = list(chunks([1, 2, 3, 4, 5], 2))
    assert chunked == [[1, 2], [3, 4], [5]]
    
    flat = list(flatten_gen([1, [2, 3], [4, [5, 6]]]))
    assert flat == [1, 2, 3, 4, 5, 6]
    
    # Test partial
    assert square(5) == 25
    assert cube(3) == 27
    
    print("All advanced function solutions verified! ✓")
Solutions - Python Tutorial | DeepML