OOP Fundamentals
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 .
(a) Implement Vector2D with: __init__, __repr__, __eq__, __hash__, __add__, __sub__, scalar __mul__ and __rmul__, and __abs__ returning the Euclidean magnitude . 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 at angle has velocity and, ignoring drag, position after time :
Using only your overloaded operators, write a function position(v0, theta, t) returning a Vector2D. Then, for , , compute the time of flight (when , ) 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):
- Consistency with local precedence order: children precede parents in the order listed in the base-class tuple.
- 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).
- Each class appears exactly once (no duplicates).
Merge computation (3 marks):
- Take head
B: not in any tail → acceptB. RemoveB: merge - Head
A: appears in tail of → reject. Next list headC: not in any tail → acceptC. RemoveC: merge - Head
A: now in no tail → acceptA.
(object implicit; full credit either way.)
(b) Return value + why (5 marks)
D().go() walks the MRO cooperatively:
D.go → super() (B) → super() (C) → super() (A).
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 has ancestor . In 's own local precedence and in every parent linearization, follows (bases come after their subclass). The merge only outputs a head when is in no tail of any remaining list. While still sits in the tail behind in some list, can never be selected. Thus is emitted before . ∎
(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 MROMerge attempt (4):
- Accept
A(no tail conflict), remove: merge - Head
X: appears in tail of → reject. Next headB: not in any tail → acceptB, remove: merge - Head
X: in tail of → reject. HeadY: in tail of → 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) # 1Marks: 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 == bthenhash(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__toNone, making instances unhashable. Any attempt to use them in asetor as adictkey raisesTypeError: 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 : Non-trivial root: For , ():
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"}
]