Functional Programming · Team Project Opens
Five problems on closures, decorators and scope, then the largest piece of work this term — the team project, in pairs, building a simplified NumPy.
Contents
What this session is for
- Walk into the mutable-default and late-binding traps deliberately
- Write decorators that actually work
- Account for any name with LEGB
- Form pairs and start the team project
Problems
14-1 The two traps
Without running anything, predict the output of each in your notes, then run them and mark the ones you got wrong:
# A
def f(x, acc=[]):
acc.append(x)
return acc
print(f(1)); print(f(2)); print(f(3))
# B
funcs = [lambda: i for i in range(3)]
print([g() for g in funcs])
# C
def g(x, acc=None):
if acc is None:
acc = []
acc.append(x)
return acc
print(g(1)); print(g(2))
# D
funcs = [lambda i=i: i for i in range(3)]
print([g() for g in funcs])
Then explain in one sentence each: why do A and C differ? Why do B and D differ?
14-2 A counting decorator
Write count_calls, recording how many times a function was called:
@count_calls
def greet(name: str) -> str:
return f"hello, {name}"
greet("Alex"); greet("Blake")
print(greet.call_count) # 2
print(greet.__name__) # must still be "greet"
print(greet.__doc__) # the original docstring must survive
Use functools.wraps.
14-3 A decorator with arguments
Write retry(times), retrying the wrapped function up to times times and re-raising the last exception if all attempts fail:
import random
@retry(times=5)
def flaky() -> str:
if random.random() < 0.7:
raise ValueError("failed")
return "succeeded"
print(flaky())
Requirements:
- print a line on each retry
- test with a fixed seed so the result reproduces (L12)
14-4 Timing plus caching
Write timed_cache, doing two things at once: caching results, and reporting whether each call was a cache hit or a real computation.
@timed_cache
def slow_square(n: int) -> int:
time.sleep(0.1)
return n * n
slow_square(4) # computed, takes 0.1 s
slow_square(4) # cache hit, essentially free
slow_square(5) # computed
Answer in your notes: for what kind of function is this decorator unsafe? Give a concrete example.
14-5 Tracing LEGB
For the code below, label which LEGB level each x belongs to in your notes, then run it to check:
x = "A"
def outer():
x = "B"
def middle():
def inner():
print("1:", x)
inner()
x_local = x
print("2:", x_local)
middle()
print("3:", x)
outer()
print("4:", x)
def tricky():
print("5:", x)
x = "C"
tricky()
That final tricky() raises. Name the exception and explain why.
14-6 Rewrite as comprehensions
Rewrite these three as comprehensions or built-ins and compare readability:
# A
from functools import reduce
total = reduce(lambda a, b: a + b, map(lambda x: x ** 2, filter(lambda x: x % 2, nums)))
# B
names = list(map(lambda p: p["name"].upper(), filter(lambda p: p["age"] >= 18, people)))
# C
longest = reduce(lambda a, b: a if len(a) >= len(b) else b, words)
Team Project: A Simplified NumPy
Worth 25% of the final grade — the largest single component Pairs of two Submitted in week 16, with a live demo
Background
L10 noted that NumPy underpins scientific computing in Python. At its centre is ndarray — a multidimensional array supporting whole-array operations without explicit loops.
This project implements a subset of it, and draws on nearly everything in the course: classes and operator overloading (L9, L10), complexity (L11), exceptions, generators (L13) and testing.
Required (70%)
An Array class supporting one and two dimensions:
class Array:
def __init__(self, data: list) -> None:
"""Construct from a nested list, validating that the shape is regular."""
# basic properties
@property
def shape(self) -> tuple[int, ...]: ...
@property
def ndim(self) -> int: ...
@property
def size(self) -> int: ...
# representation
def __repr__(self) -> str: ...
def __eq__(self, other: object) -> bool: ...
# indexing
def __getitem__(self, key): ... # support a[0], a[1, 2], a[0:2]
def __setitem__(self, key, value): ...
# elementwise arithmetic
def __add__(self, other): ... # Array + Array, or Array + scalar
def __sub__(self, other): ...
def __mul__(self, other): ... # elementwise, not matrix multiplication
def __truediv__(self, other): ...
def __neg__(self): ...
# reductions
def sum(self, axis: int | None = None): ...
def mean(self, axis: int | None = None): ...
def max(self, axis: int | None = None): ...
def min(self, axis: int | None = None): ...
# shape
def reshape(self, *shape: int) -> "Array": ...
def transpose(self) -> "Array": ...
Plus a few module-level functions:
def zeros(*shape: int) -> Array: ...
def ones(*shape: int) -> Array: ...
def arange(n: int) -> Array: ...
def dot(a: Array, b: Array) -> Array: ... # matrix multiplication
Optional (+5% each, up to +30%)
- Broadcasting: shapes
(3,1)and(1,4)add to give(3,4) - Boolean indexing:
a[a > 5]returns every element above 5 - More dimensions: three and beyond
- Performance: replace nested lists with the
arraymodule or a flat list plus strides, and measure the difference __format__and pretty printing: aligned output, as NumPy does
Marks
| Component | Weight |
|---|---|
| required functionality | 40% |
| tests | 15% |
| code quality and design | 10% |
| documentation (README) | 5% |
| technical report | 10% |
| optional extensions | up to +30% (capped at 100% overall) |
| total | 80% plus extensions |
Working as a pair
Both of you must write code. Your submission must include a division-of-work statement naming who did what.
The project is submitted, not defended — but both your names are on it, and both of you must be able to explain every part of what you hand in. “My partner wrote that part, I am not sure” is not an acceptable answer.
Due this week (unmarked, but required)
- Your pair: both names and student numbers
- A one-page design sketch: how you intend to store the data (nested lists? a flat list plus a shape?), how you will divide the work, and which optional parts you plan to attempt
Before you submit
- 14-1 predicted first, then ran, with the wrong guesses marked
- 14-2, 14-3 and 14-4 all use
functools.wraps - 14-4 answers what kind of function it is unsafe for
- 14-5 labels every x and explains the exception
- 14-6 rewrites all three
- Team project: pair registered
- Team project: one-page design sketch submitted
- AI usage declaration written