CS1602Introduction to Computation
Lab 14Part 6 Pythonic Python and Modern PracticeAI Level 1

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.

Due:Homework 5 · Tue 29 Dec, 23:59
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 array module or a flat list plus strides, and measure the difference
  • __format__ and pretty printing: aligned output, as NumPy does

Marks

ComponentWeight
required functionality40%
tests15%
code quality and design10%
documentation (README)5%
technical report10%
optional extensionsup to +30% (capped at 100% overall)
total80% 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)

  1. Your pair: both names and student numbers
  2. 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