Python Intermediate
Level: 4 — Application (novel problems, no hints) Time limit: 60 minutes Total marks: 50
Answer all questions. Write complete, runnable Python 3 code where asked. Assume no external packages unless the standard library is stated.
Question 1 — Log Parsing & Aggregation (12 marks)
A server writes lines to access.log in the format:
2024-03-01T09:15:22 GET /home 200
2024-03-01T09:15:23 POST /login 401
2024-03-01T09:16:01 GET /home 200
Each line is <ISO timestamp> <METHOD> <path> <status>.
Write a function top_paths(filename, n) that:
(a) Opens the file safely using a context manager and reads it line by line (do not load the whole file into memory at once). (3)
(b) Uses a regular expression with named groups to extract method, path, and status. Skip any line that does not match. (4)
(c) Counts requests per path but only for lines where status == "200", using an appropriate collections tool. (3)
(d) Returns the n most common paths as a list of (path, count) tuples, highest first. (2)
Question 2 — A Retry Decorator (11 marks)
Write a decorator retry(times, exceptions) that re-runs the decorated function up to times total attempts if it raises any exception listed in the tuple exceptions.
(a) The decorator must accept arguments and preserve the wrapped function's __name__ and __doc__. (4)
(b) If all attempts fail, re-raise the last exception caught. (3)
(c) On any attempt other than the last, print Attempt <k> failed: <message> before retrying. (2)
(d) Show a usage example decorating a function fetch() that should retry up to 3 times on ValueError only. (2)
Question 3 — Generators & Iterators (10 marks)
(a) Write a generator function running_average() that uses send(): each value sent in returns the running mean of all values received so far. The first next() primes it and yields None. (5)
(b) Write an iterator class Countdown(start) implementing __iter__ and __next__ that yields start, start-1, …, 1 and then raises StopIteration. (5)
Question 4 — Exceptions & Custom Hierarchy (9 marks)
You are validating user config dictionaries.
(a) Define a base custom exception ConfigError and two subclasses MissingKeyError and BadTypeError. (3)
(b) Write validate(cfg) that requires key "port" (an int) and key "host" (a str). Raise MissingKeyError if a key is absent and BadTypeError if a value has the wrong type, each with a helpful message. (4)
(c) Explain what except ConfigError as e: will catch and why, referencing the exception hierarchy. (2)
Question 5 — Predict the Output (8 marks)
For each snippet, state exactly what is printed (or the exception raised). Justify briefly.
(a) (2)
import itertools
print(list(itertools.accumulate([1, 2, 3, 4], lambda a, b: a * b)))(b) (2)
from functools import reduce
print(reduce(lambda a, b: a + b, [], 10))(c) (2)
def gen():
try:
yield 1
yield 2
finally:
print("cleanup")
g = gen()
print(next(g))
g.close()(d) (2)
import re
print(re.sub(r'(\w)\1', 'X', 'bookkeeper'))Answer keyMark scheme & solutions
Question 1 (12 marks)
import re
from collections import Counter
LINE = re.compile(r'^\S+ (?P<method>\w+) (?P<path>\S+) (?P<status>\d+)$')
def top_paths(filename, n):
counts = Counter()
with open(filename) as f: # (a) context manager
for line in f: # (a) line-by-line, lazy
m = LINE.match(line.strip())
if not m: # (b) skip non-matching
continue
if m.group('status') == '200': # (c) filter
counts[m.group('path')] += 1
return counts.most_common(n) # (d)Marks:
- (a)
with open(...)(1) + iteratingfor line in frather thanread()/readlines()(2) = 3. - (b) regex with named groups
(?P<name>...)(2); usingmatch/searchand skipping onNone(2) = 4. - (c)
Counter(ordefaultdict(int)) (2); only countingstatus == "200"(1) = 3. - (d)
most_common(n)returns list of tuples sorted descending (2).
Why: iterating the file object streams lines so memory stays O(1); named groups make extraction self-documenting; Counter.most_common already sorts by count descending.
Question 2 (11 marks)
import functools
def retry(times, exceptions):
def decorator(func):
@functools.wraps(func) # (a) preserves name/doc
def wrapper(*args, **kwargs):
last = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as e: # (b) only listed exceptions
last = e
if attempt < times: # (c) not last attempt
print(f"Attempt {attempt} failed: {e}")
raise last # (b) re-raise last
return wrapper
return decorator
# (d) usage
@retry(3, (ValueError,))
def fetch():
"""Fetch data, may fail."""
...Marks:
- (a) three nested layers (args → func → wrapper) (2);
@functools.wraps(func)(2) = 4. - (b)
except exceptions as ecapturing only the tuple (1); storinglastandraise lastafter loop (2) = 3. - (c) conditional print with correct attempt number only when
attempt < times(2). - (d) correct
@retry(3, (ValueError,))— tuple with single exception (2).
Why: three levels are needed because the decorator itself takes arguments; wraps copies __name__/__doc__; re-raising the stored exception after the loop preserves the original error type.
Question 3 (10 marks)
(a)
def running_average():
total = 0
count = 0
value = yield None # prime: first next() yields None
while True:
total += value
count += 1
value = yield total / countMarks: initial yield None on priming (2); correct accumulation of total & count (2); yielding the mean back to the caller (1) = 5.
Usage check: g=running_average(); next(g) → None; g.send(2) → 2.0; g.send(4) → 3.0.
(b)
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
val = self.current
self.current -= 1
return valMarks: __iter__ returning self (2); __next__ decrementing and returning (2); raise StopIteration at end (1) = 5.
Why: an iterator must return itself from __iter__; StopIteration is the signal that ends a for loop cleanly.
Question 4 (9 marks)
(a)
class ConfigError(Exception):
pass
class MissingKeyError(ConfigError):
pass
class BadTypeError(ConfigError):
passMarks: base subclassing Exception (1); both subclasses inheriting from ConfigError (2) = 3.
(b)
def validate(cfg):
required = {"port": int, "host": str}
for key, typ in required.items():
if key not in cfg:
raise MissingKeyError(f"Missing required key: {key!r}")
if not isinstance(cfg[key], typ):
raise BadTypeError(
f"Key {key!r} must be {typ.__name__}, got {type(cfg[key]).__name__}")Marks: missing-key check → MissingKeyError (2); type check via isinstance → BadTypeError (2) = 4.
(Note: bool is a subclass of int; accepting or excepting it explicitly earns full marks if mentioned.)
(c) except ConfigError as e: catches any of ConfigError, MissingKeyError, and BadTypeError, because both subclasses inherit from ConfigError; an except clause matches the named class and all its descendants in the exception hierarchy. (2)
Question 5 (8 marks)
(a) [1, 2, 6, 24] — accumulate with a multiply function gives running products: 1, 1·2, 2·3, 6·4. (2)
(b) 10 — reduce on an empty iterable with an initializer returns the initializer unchanged (the function is never called). (2)
(c) Prints:
1
cleanup
next(g) yields 1; g.close() throws GeneratorExit at the suspended yield, running the finally block which prints cleanup. No 2 is produced. (2)
(d) Xokkeeper → wait: apply carefully. Pattern (\w)\1 matches a doubled character; re.sub replaces the leftmost non-overlapping matches. bookkeeper: oo→X, then kk→X, then ee→X gives bXXpXpr. Answer: bXXpXpr. (2)
Why (d): scanning left→right non-overlapping: b, oo(match), kk(match), ee(match), p, r → b X X p X p r = bXXpXpr.
[
{"claim":"Q5a accumulate product gives [1,2,6,24]","code":"import itertools; result = list(itertools.accumulate([1,2,3,4], lambda a,b: a*b)) == [1,2,6,24]"},
{"claim":"Q5b reduce of empty list with init 10 is 10","code":"from functools import reduce; result = reduce(lambda a,b:a+b, [], 10) == 10"},
{"claim":"Q5d re.sub of doubled chars in bookkeeper gives bXXpXpr","code":"import re; result = re.sub(r'(\\w)\\1','X','bookkeeper') == 'bXXpXpr'"},
{"claim":"Q3a running_average sends give correct means","code":"def running_average():\n total=0; count=0; value=yield None\n while True:\n total+=value; count+=1; value=yield total/count\ng=running_average(); next(g); a=g.send(2); b=g.send(4); result = (a==2.0 and b==3.0)"},
{"claim":"Q3b Countdown(3) yields [3,2,1]","code":"class Countdown:\n def __init__(s,start): s.current=start\n def __iter__(s): return s\n def __next__(s):\n if s.current<=0: raise StopIteration\n v=s.current; s.current-=1; return v\nresult = list(Countdown(3))==[3,2,1]"}
]