ML Algorithms
Python
25 / 29

Advanced Python

The features that separate scripting from engineering: iterators, generators, decorators, context managers, and async.

Why this matters
ML pipelines stream millions of rows, retry flaky network calls, and run on async web frameworks. These features let you write code that is fast, memory-friendly, and idiomatic.

1. The Data Model & Dunder Methods

Python's behavior is driven by __dunder__ methods. Implementing them lets your objects behave like built-ins — iterable, indexable, callable, comparable.

class Vector:
    def __init__(self, *components): self.c = components
    def __len__(self):       return len(self.c)
    def __getitem__(self, i): return self.c[i]
    def __add__(self, other): return Vector(*(a+b for a,b in zip(self.c, other.c)))
    def __repr__(self):       return f"Vector{self.c}"

v = Vector(1, 2, 3) + Vector(4, 5, 6)   # Vector(5, 7, 9)
print(len(v), v[0])                      # 3 5

2. Iterators & Generators

Generators produce values lazily — perfect for huge files and infinite streams. They use constant memory regardless of input size.

def read_jsonl(path):
    with open(path) as f:
        for line in f:           # lazy: one line at a time
            yield json.loads(line)

# Process 50 GB without loading it into RAM
for record in read_jsonl("events.jsonl"):
    process(record)

Generator expressions are the lazy cousin of list comprehensions:

total = sum(x*x for x in range(10_000_000))   # no list built

3. Decorators

A decorator is a function that wraps another function. Used for caching, timing, auth, retries, logging — everywhere.

import time, functools

def timed(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        t0 = time.perf_counter()
        out = fn(*args, **kwargs)
        print(f"{fn.__name__} took {time.perf_counter()-t0:.3f}s")
        return out
    return wrapper

@timed
def train_model(X, y): ...

4. Context Managers

with blocks guarantee setup/teardown — even on exceptions. Build your own with contextlib:

from contextlib import contextmanager

@contextmanager
def timer(label):
    t0 = time.perf_counter()
    try:    yield
    finally: print(f"{label}: {time.perf_counter()-t0:.3f}s")

with timer("inference"):
    preds = model.predict(X)

5. Async / Await

Async lets a single thread juggle thousands of I/O-bound tasks (HTTP calls, DB queries). It does not speed up CPU-bound work.

import asyncio, aiohttp

async def fetch(session, url):
    async with session.get(url) as r:
        return await r.json()

async def main(urls):
    async with aiohttp.ClientSession() as s:
        return await asyncio.gather(*(fetch(s, u) for u in urls))

results = asyncio.run(main(urls))   # 1000 calls in parallel

6. Performance Tools

  • functools.lru_cache — memoize pure functions
  • dataclasses — boilerplate-free value classes
  • __slots__ — cut per-instance memory by ~40%
  • collections.deque / Counter / defaultdict — O(1) primitives
  • itertools — chain, groupby, islice, product
Rule of thumb
Prefer generators over lists for pipelines. Prefer async for I/O, multiprocessing for CPU. Reach for a decorator before copy-pasting wrapper code.