python
solutions
solutions.py🐍python
"""
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! ✓")