3. Functional Patterns

Compose behavior with generators, caching, and higher-order functions for clarity and performance.

Question: How can you create a context manager besides using a class?

Answer: The @contextlib.contextmanager decorator allows you to create a context manager from a simple generator function. This is often more concise than writing a full class with __enter__ and __exit__.

Explanation: The code before the yield statement acts as the setup (__enter__), the yield provides the resource, and the code in a finally block after the yield serves as the cleanup (__exit__), guaranteeing resource release even if an exception occurs. For managing a dynamic number of contexts, contextlib.ExitStack is a powerful utility.

from contextlib import contextmanager

@contextmanager
def open_readonly(path):
    f = open(path, "rb")
    try:
        yield f
    finally:
        f.close()

Question: What is yield from and when is it useful?

Answer: yield from delegates part of a generator’s operations to a subgenerator, simplifying generator composition and automatically forwarding .send(), .throw(), and .close().

Explanation: It reduces boilerplate when building pipelines.

def read_lines(paths):
    for p in paths:
        with open(p) as f:
            yield from f  # delegate iteration

Question: When should you use functools.lru_cache?

Answer: Use it to memoize pure functions where inputs fully determine outputs and cache size is bounded.

Explanation: It trades memory for speed and requires hashable arguments.

from functools import lru_cache

@lru_cache(maxsize=1024)
def parse_date(s: str) -> datetime:
    return expensive_parse(s)

Question: How does partial application help in Python?

Answer: functools.partial pre-binds some arguments, producing simpler call sites and aiding higher-order APIs.

Explanation: It’s useful in callbacks and pipelines.

from functools import partial
strip_commas = partial(str.replace, old=",", new="")