Level 5 — MasteryOOP Fundamentals

OOP Fundamentals

75 minutes60 marksprintable — key stays hidden on paper

Subject: Coding · Chapter: 2.1 OOP Fundamentals Time limit: 75 minutes · Total marks: 60 Instructions: Answer all questions. Write idiomatic Python 3. Justify design choices. Where a proof or derivation is asked, argue rigorously.


Question 1 — C3 Linearization, super(), and cooperative dispatch (24 marks)

Consider the diamond hierarchy:

class A:
    def go(self): return ["A"]
class B(A):
    def go(self): return ["B"] + super().go()
class C(A):
    def go(self): return ["C"] + super().go()
class D(B, C):
    def go(self): return ["D"] + super().go()

(a) State the three consistency constraints that Python's C3 linearization guarantees, then compute the MRO of D by hand, showing every merge step. (6)

(b) Predict the exact return value of D().go(). Explain why super() inside B.go dispatches to C.go (not A.go) even though B does not inherit from C. (5)

(c) Prove the following claim or give a counterexample: "For any class hierarchy where C3 linearization succeeds, every class in the MRO appears exactly once, and a class always precedes all of its ancestors." Reason from the C3 merge rule. (5)

(d) Construct a hierarchy of four classes for which C3 linearization fails (raises TypeError). Show the merge attempt reaching a deadlock, and explain the precise condition that makes no head selectable. (8)


Question 2 — A physically-correct Vector2D via operator overloading (22 marks)

You are modelling 2D kinematics. Build an immutable value type Vector2D storing components x,yx, y.

(a) Implement Vector2D with: __init__, __repr__, __eq__, __hash__, __add__, __sub__, scalar __mul__ and __rmul__, and __abs__ returning the Euclidean magnitude v=x2+y2\lvert v\rvert=\sqrt{x^2+y^2}. Enforce immutability using __slots__ and a read-only @property for x and y. (10)

(b) Explain the contract linking __eq__ and __hash__. Why would defining __eq__ without __hash__ make instances unusable as dict keys or set members in Python 3? (4)

(c) A projectile launched with speed v0v_0 at angle θ\theta has velocity v=(v0cosθ,  v0sinθ)\mathbf{v}=(v_0\cos\theta,\;v_0\sin\theta) and, ignoring drag, position after time tt: r(t)=vt+12gt2,g=(0,9.81).\mathbf{r}(t)=\mathbf{v}\,t + \tfrac{1}{2}\mathbf{g}\,t^2,\qquad \mathbf{g}=(0,-9.81). Using only your overloaded operators, write a function position(v0, theta, t) returning a Vector2D. Then, for v0=20 m/sv_0=20\ \mathrm{m/s}, θ=45\theta=45^\circ, compute the time of flight TT (when y=0y=0, t>0t>0) symbolically and give its numeric value. (8)


Question 3 — Abstract accounts, properties, and encapsulation (14 marks)

Design a small banking module.

(a) Define an abstract base class Account (using ABC) with an abstract method interest_rate(self) -> float. Give it a private balance via name mangling (__balance), a @property balance (getter only), and a deposit/withdraw interface where withdraw raises ValueError on overdraft. (8)

(b) Subclass SavingsAccount(rate=0.04) and CheckingAccount(rate=0.0) overriding interest_rate. Write a polymorphic free function apply_interest(accounts) that credits one period of interest to each account via duck typing — it must not check types. Explain how name mangling still lets a subclass method touch the parent's __balance (or explain why it cannot). (6)

Answer keyMark scheme & solutions

Question 1

(a) C3 constraints + MRO of D (6 marks)

Constraints (3 marks, 1 each):

  1. Consistency with local precedence order: children precede parents in the order listed in the base-class tuple.
  2. Monotonicity: the MRO of a class is consistent with the MROs of its parents (a linearization that holds for a parent must hold for the child).
  3. Each class appears exactly once (no duplicates).

Merge computation (3 marks): L[A]=[A],L[B]=[B,A],L[C]=[C,A]L[A]=[A],\quad L[B]=[B,A],\quad L[C]=[C,A] L[D]=D+merge(L[B],L[C],[B,C])L[D]=D + \text{merge}(L[B],L[C],[B,C]) =D+merge([B,A],[C,A],[B,C])=D+\text{merge}([B,A],[C,A],[B,C])

  • Take head B: not in any tail → accept B. Remove B: merge([A],[C,A],[C])([A],[C,A],[C])
  • Head A: appears in tail of [C,A][C,A] → reject. Next list head C: not in any tail → accept C. Remove C: merge([A],[A],[])([A],[A],[])
  • Head A: now in no tail → accept A.

L[D]=[D,  B,  C,  A,  object]\boxed{L[D]=[D,\;B,\;C,\;A,\;\text{object}]} (object implicit; full credit either way.)

(b) Return value + why (5 marks)

D().go() walks the MRO cooperatively: D.gosuper() (B) → super() (C) → super() (A). ["D","B","C","A"](2)\texttt{["D","B","C","A"]}\quad(2) Why B→C (3): super() is bound to the next class after the current one in the instance's MRO, not to the static parent. Inside B.go, super() resolves against type(self).__mro__ = D's MRO [D,B,C,A]; the class after B is C. This is the essence of cooperative multiple inheritance — each class defers to the next in linearization order, so A.go runs exactly once.

(c) Proof (5 marks)

Claim is TRUE.

  • Uniqueness (2): The merge rule removes a selected head from all lists in which it appears before emitting it; C3 only succeeds if it consumes every list. Hence no class is emitted twice → each appears once.
  • Ancestor-after-descendant (3): Suppose class XX has ancestor YY. In XX's own local precedence and in every parent linearization, YY follows XX (bases come after their subclass). The merge only outputs a head HH when HH is in no tail of any remaining list. While YY still sits in the tail behind XX in some list, YY can never be selected. Thus XX is emitted before YY. ∎

(d) Failing hierarchy (8 marks)

class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass   # TypeError: cannot create a consistent MRO

Merge attempt (4): L[A]=[A,X,Y],L[B]=[B,Y,X]L[A]=[A,X,Y],\quad L[B]=[B,Y,X] L[C]=C+merge([A,X,Y],[B,Y,X],[A,B])L[C]=C+\text{merge}([A,X,Y],[B,Y,X],[A,B])

  • Accept A (no tail conflict), remove: merge([X,Y],[B,Y,X],[B])([X,Y],[B,Y,X],[B])
  • Head X: appears in tail of [B,Y,X][B,Y,X] → reject. Next head B: not in any tail → accept B, remove: merge([X,Y],[Y,X],[])([X,Y],[Y,X],[])
  • Head X: in tail of [Y,X][Y,X] → reject. Head Y: in tail of [X,Y][X,Y] → reject. Deadlock. (3)

Condition (1): No candidate head is "in the tail of no list" because X precedes Y in one linearization while Y precedes X in the other — the local precedence orders are mutually contradictory, so no consistent global order exists.


Question 2

(a) Vector2D (10 marks)

import math
class Vector2D:
    __slots__ = ("_x", "_y")                       # 1: immutability scaffold
    def __init__(self, x, y):
        object.__setattr__(self, "_x", float(x))
        object.__setattr__(self, "_y", float(y))
    @property
    def x(self): return self._x                    # 1: read-only
    @property
    def y(self): return self._y
    def __repr__(self): return f"Vector2D({self._x}, {self._y})"   # 1
    def __eq__(self, o):                            # 1
        return isinstance(o, Vector2D) and (self._x, self._y) == (o._x, o._y)
    def __hash__(self): return hash((self._x, self._y))            # 1
    def __add__(self, o): return Vector2D(self._x+o._x, self._y+o._y)  # 1
    def __sub__(self, o): return Vector2D(self._x-o._x, self._y-o._y)  # 1
    def __mul__(self, s): return Vector2D(self._x*s, self._y*s)        # 1
    __rmul__ = __mul__                                                 # 1
    def __abs__(self): return math.hypot(self._x, self._y)            # 1

Marks: slots/immutability (1), properties (1), repr (1), eq (1), hash (1), add (1), sub (1), mul (1), rmul (1), abs (1).

(b) eq/hash contract (4 marks)

  • Contract (2): if a == b then hash(a) == hash(b) must hold; hashing partitions objects into buckets and equality is checked within a bucket — a violation loses keys.
  • Python 3 behaviour (2): defining __eq__ sets __hash__ to None, making instances unhashable. Any attempt to use them in a set or as a dict key raises TypeError: unhashable type. So we must explicitly redefine __hash__ (as done above) using the same fields as __eq__.

(c) Kinematics (8 marks)

def position(v0, theta, t):
    v = Vector2D(v0*math.cos(theta), v0*math.sin(theta))   # 2
    g = Vector2D(0, -9.81)
    return v*t + g*(0.5*t*t)                                # 2  (uses overloads)

Time of flight (4): set y(t)=0y(t)=0: v0sinθt12(9.81)t2=0t(v0sinθ4.905t)=0.v_0\sin\theta\,t - \tfrac12(9.81)t^2 = 0 \Rightarrow t\big(v_0\sin\theta - 4.905\,t\big)=0. Non-trivial root: T=2v0sinθ9.81.T=\frac{2v_0\sin\theta}{9.81}. For v0=20v_0=20, θ=45\theta=45^\circ (sin45°=22\sin45°=\tfrac{\sqrt2}{2}): T=220229.81=2029.812.883 s.T=\frac{2\cdot20\cdot\frac{\sqrt2}{2}}{9.81}=\frac{20\sqrt2}{9.81}\approx 2.883\ \text{s}.


Question 3

(a) Abstract Account (8 marks)

from abc import ABC, abstractmethod
class Account(ABC):
    def __init__(self, balance=0.0):
        self.__balance = float(balance)            # 1: name-mangled private
    @property
    def balance(self):                             # 2: getter only, no setter
        return self.__balance
    def deposit(self, amt):
        if amt <= 0: raise ValueError("amount must be positive")   # 1
        self.__balance += amt
    def withdraw(self, amt):
        if amt > self.__balance:
            raise ValueError("insufficient funds")  # 2: overdraft guard
        self.__balance -= amt
    @abstractmethod
    def interest_rate(self) -> float: ...          # 2: abstract → Account uninstantiable

(b) Subclasses + polymorphism (6 marks)

class SavingsAccount(Account):
    def interest_rate(self): return 0.04           # 1
class CheckingAccount(Account):
    def interest_rate(self): return 0.0            # 1
 
def apply_interest(accounts):                      # 2: duck typing, no isinstance
    for a in accounts:
        a.deposit(a.balance * a.interest_rate())

Name mangling explanation (2): __balance in Account is compiled to _Account__balance. A subclass method does not automatically see self.__balance (that would mangle to _Savings__balance — a different name). Therefore subclasses cannot touch the parent's private field directly; they must use the public deposit/withdraw/balance API — which is exactly the encapsulation intended. Full marks require noting the mangling is name-based (not true privacy): a._Account__balance still works if forced.

[
  {"claim":"D MRO computed by C3 equals [D,B,C,A,object]",
   "code":"class A: pass\nclass B(A): pass\nclass C(A): pass\nclass D(B,C): pass\nresult = [c.__name__ for c in D.__mro__] == ['D','B','C','A','object']"},
  {"claim":"Cooperative super chain yields ['D','B','C','A']",
   "code":"class A:\n    def go(self): return ['A']\nclass B(A):\n    def go(self): return ['B']+super().go()\nclass C(A):\n    def go(self): return ['C']+super().go()\nclass D(B,C):\n    def go(self): return ['D']+super().go()\nresult = D().go() == ['D','B','C','A']"},
  {"claim":"Time of flight T = 20*sqrt(2)/9.81 ~ 2.883 s",
   "code":"t=symbols('t',positive=True)\nv0=20; th=pi/4\nexpr=v0*sin(th)*t-Rational(981,200)*t*t\nroots=solve(Eq(expr,0),t)\nT=[r for r in roots if r>0][0]\nresult = abs(float(T)-2.883)<0.01"},
  {"claim":"Vector operator overloads: 2*(1,2)+( -1,3)=(1,7) and |(3,4)|=5",
   "code":"import math\nvx,vy=2*1+(-1),2*2+3\nresult = (vx,vy)==(1,7) and math.hypot(3,4)==5.0"}
]