§ 2.6section 6 of 20·~10 min
Pythonic code
"Pythonic" code uses the language's built-in tools instead of re-inventing them. You'll read a lot of it in AI libraries.
enumerate and zip
for i, book in enumerate(books, start=1): # counter + item
print(i, book.title)
names = ["Asha", "Ravi"]
scores = [91, 78]
for name, score in zip(names, scores): # walk two lists together
print(name, score)
dict(zip(names, scores)) # {'Asha': 91, 'Ravi': 78}
Comprehensions (a recap with dicts and sets)
titles = [b.title for b in books if b.available]
by_isbn = {b.isbn: b for b in books} # dict comprehension: fast lookup table
authors = {b.author for b in books} # set comprehension: unique authors
Lambdas and sorting with a key
A lambda is a tiny unnamed function, mostly used as a key for sorting:
books.sort(key=lambda b: b.title) # sort by title
top = sorted(students, key=lambda s: s.average, reverse=True)[:3]
max(expenses, key=lambda e: e["amount"]) # the biggest expense
If the logic needs more than one simple expression, write a normal def.
Generators: one item at a time
A function that uses yield instead of return becomes a generator. It produces values lazily, one per request, instead of building the whole list in memory:
def read_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.strip()
for line in read_lines("huge_log.txt"): # a 5 GB file is fine: only one line is in memory
if "ERROR" in line:
print(line)
squares = (n * n for n in range(10**9)) # a generator expression: note the ( ) not [ ]
next(squares) # 0
This matters later: LLM streaming responses (Week 8) arrive as an iterator of small chunks that you loop over as they arrive, exactly like a generator. Streaming large files through an embedding model in batches (Week 10) uses the same idea.
Decorators (intro)
A decorator is a function that wraps another function to add behaviour. The @ line is shorthand:
import time
from functools import wraps
def timed(fn):
@wraps(fn) # keep the original name and docstring
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
print(f"{fn.__name__} took {time.perf_counter() - start:.3f}s")
return result
return wrapper
@timed # same as: slow_add = timed(slow_add)
def slow_add(a, b):
time.sleep(0.5)
return a + b
slow_add(2, 3) # prints "slow_add took 0.501s", then returns 5