Python Intermediate
Chapter: 1.3 Python Intermediate Difficulty: Level 5 — Mastery (cross-domain: math + physics + coding, build & prove) Time limit: 90 minutes Total marks: 60
Instructions: Write clean, runnable Python 3 code. You may assume standard library only. Where a proof or complexity argument is requested, justify rigorously. Partial credit is awarded for correct reasoning even if code is incomplete.
Question 1 — Streaming Numerical Integration Engine (24 marks)
You must build a memory-bounded engine that estimates the definite integral using the composite Simpson's rule over subintervals ( even), where the sample points are produced lazily and consumed once (streaming), so the whole array is never stored.
Recall composite Simpson's rule with step and nodes :
(a) Write a generator nodes(a, b, n) that yields the tuples (k, x_k) for k = 0..n without building any list. (4)
(b) Write a decorator @count_calls (using functools.wraps) that counts how many times the decorated function is invoked and stores it on an attribute .calls. Apply it to the integrand. (5)
(c) Write simpson(f, a, b, n) that consumes nodes and returns the estimate. It must:
- raise a custom exception
IntegrationError(subclass ofValueError) ifnis odd orn <= 0; - call
fexactly times (verify with the counter from part (b)). (7)
(d) For on , compute the estimate with . The exact value is . Report the estimate to 6 decimals and the absolute error. (4)
(e) Prove that for a fixed even , your streaming implementation uses auxiliary memory (independent of ), and state the error term order of composite Simpson's rule in terms of . (4)
Question 2 — Physics Log Parser with Context Manager (22 marks)
A sensor writes projectile-motion log lines to a file. Each valid line has the form:
t=1.50s v=12.30m/s
(time in seconds, speed in m/s). Some lines are corrupted.
(a) Write a regular expression and a function parse_line(line) that returns (t, v) as floats, or raises ValueError for a malformed line. Use named groups. (6)
(b) Build a class-based context manager LogReader implementing __enter__ and __exit__ that opens a file in read mode, and on exit guarantees the file is closed even if an exception is raised inside the with block. __exit__ must not suppress exceptions. (6)
(c) Using LogReader, write average_speed(path) that reads line-by-line (not readlines()), skips malformed lines using try/except, and returns the mean speed. Use else/finally appropriately: count total lines read in finally. (6)
(d) A projectile launched at speed at angle has range . Given a parsed launch speed , , , compute using math. Report to 3 decimals. (4)
Question 3 — Fibonacci Iterator & Golden-Ratio Convergence (14 marks)
(a) Implement a class-based iterator Fib with __iter__/__next__ that yields Fibonacci numbers and raises StopIteration after producing limit terms. (5)
(b) Using itertools.islice, collect the first 15 ratios and show they approach the golden ratio . Report to 6 decimals. (5)
(c) Prove that if converges to a limit , then . (Use the recurrence .) (4)
Answer keyMark scheme & solutions
Question 1 (24 marks)
(a) (4)
def nodes(a, b, n):
h = (b - a) / n
for k in range(n + 1):
yield k, a + k * h- Correct
yieldof(k, x_k)(2); usesrange(n+1)inclusive of endpoint (1); no list built — generator (1).
(b) (5)
import functools
def count_calls(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
wrapper.calls += 1
return fn(*args, **kwargs)
wrapper.calls = 0
return wrapperfunctools.wraps(1); attribute.callsinitialized (1); increments per call (2); returns result (1). Whywraps: preserves__name__/__doc__of wrapped function.
(c) (7)
class IntegrationError(ValueError):
pass
def simpson(f, a, b, n):
if n <= 0 or n % 2 != 0:
raise IntegrationError("n must be positive and even")
h = (b - a) / n
total = 0.0
for k, x in nodes(a, b, n):
fx = f(x)
if k == 0 or k == n:
total += fx
elif k % 2 == 1:
total += 4 * fx
else:
total += 2 * fx
return total * h / 3- Custom exception subclass of
ValueError(2); validation of odd/non-positiven(1); correct endpoint/odd/even weights 1/4/2 (2);fcalled exactly once per node ⇒ calls (1); returns sum (1).
(d) (4) With , , , nodes : Sum ; times ⇒ . Exact . Absolute error .
- Estimate 0.632334 (2); exact 0.632121 (1); error 0.000213 (1).
(e) (4)
- Memory: The generator holds only
k,x,h, and running scalartotal— a fixed constant number of variables regardless ofn. No list of nodes or samples is materialized, so auxiliary space is . (2) - Error order: Composite Simpson's rule has global error (equivalently ), so error scales as . (2)
Question 2 (22 marks)
(a) (6)
import re
PATTERN = re.compile(r"t=(?P<t>-?\d+(?:\.\d+)?)s\s+v=(?P<v>-?\d+(?:\.\d+)?)m/s")
def parse_line(line):
m = PATTERN.fullmatch(line.strip())
if not m:
raise ValueError(f"malformed: {line!r}")
return float(m.group("t")), float(m.group("v"))- Correct regex with named groups
t,v(3);fullmatch/anchoring (1); raisesValueErroron failure (1); float conversion (1).
(b) (6)
class LogReader:
def __init__(self, path):
self.path = path
self.f = None
def __enter__(self):
self.f = open(self.path, "r")
return self.f
def __exit__(self, exc_type, exc, tb):
if self.f:
self.f.close()
return False # do not suppress__enter__opens read mode, returns file (2);__exit__closes file (2); closes even on exception because__exit__always runs (1); returnsFalseso exceptions propagate (1).
(c) (6)
def average_speed(path):
total, count, lines = 0.0, 0, 0
with LogReader(path) as f:
for line in f:
lines += 1
try:
_, v = parse_line(line)
except ValueError:
continue
else:
total += v
count += 1
if count == 0:
raise ValueError("no valid lines")
return total / count- Line-by-line iteration, not
readlines()(2);try/except ValueErrorskips malformed (2);elseused for the valid-line accumulation (1); counts total lines (in loop / could usefinally) (1). Whyelse: runs only when no exception, cleanly separating success path.
(d) (4)
import math
R = 20**2 * math.sin(math.radians(90)) / 9.81 # 40.774719...Report m.
- Formula applied (1); (1); (1); value 40.775 (1).
Question 3 (14 marks)
(a) (5)
class Fib:
def __init__(self, limit):
self.limit = limit
self.a, self.b = 1, 1
self.count = 0
def __iter__(self):
return self
def __next__(self):
if self.count >= self.limit:
raise StopIteration
self.count += 1
val = self.a
self.a, self.b = self.b, self.a + self.b
return val__iter__returns self (1);__next__advances state (2); correct sequence 1,1,2,3,5… (1);StopIterationafterlimit(1).
(b) (5)
import itertools
fibs = list(itertools.islice(Fib(16), 16)) # F1..F16
ratios = [fibs[k+1]/fibs[k] for k in range(15)]
print(fibs[15]/fibs[14]) # F16/F15, ⇒ ratio . .
- Uses
islice(2); computes 15 ratios (1); (1); matches (1).
(c) (4) Divide the recurrence by : If (with ), then taking limits: , i.e. . Solving: . Since ratios are positive, take the positive root: . ∎
- Divide recurrence (1); limit equation (1); quadratic (1); positive root selection = (1).
[
{"claim":"Simpson estimate for e^-x on [0,1], n=2 is ~0.632334",
"code":"import sympy as sp; h=sp.Rational(1,2); f=lambda x: sp.exp(-x); est=h/3*(f(0)+4*f(sp.Rational(1,2))+f(1)); result=abs(float(est)-0.632334)<1e-5"},
{"claim":"Absolute error vs exact 1-e^-1 is ~0.000213",
"code":"import sympy as sp; h=sp.Rational(1,2); est=h/3*(sp.exp(0)+4*sp.exp(sp.Rational(-1,2))+sp.exp(-1)); exact=1-sp.exp(-1); result=abs(float(abs(est-exact))-0.000213)<5e-6"},
{"claim":"Projectile range R = 400/9.81 = 40.775 m at 45 deg",
"code":"import math; R=20**2*math.sin(math.radians(90))/9.81; result=abs(R-40.775)<1e-3"},
{"claim":"F16/F15 = 987/610 approx 1.618033",
"code":"import sympy as sp; result=abs(float(sp.Rational(987,610))-1.618033)<1e-5"},
{"claim":"Golden ratio solves L^2-L-1=0 positive root",
"code":"import sympy as sp; L=sp.symbols('L'); sols=sp.solve(L**2-L-1,L); phi=(1+sp.sqrt(5))/2; result=any(sp.simplify(s-phi)==0 for s in sols)"}
]