OOP Fundamentals
Level 4 — Application (novel problems, no hints) Time limit: 60 minutes Total marks: 50
Answer all questions. Write valid Python 3 where code is requested. Assume standard library only.
Question 1 — MRO & super() (10 marks)
Consider the following class hierarchy:
class A:
def who(self):
return "A"
class B(A):
def who(self):
return "B->" + super().who()
class C(A):
def who(self):
return "C->" + super().who()
class D(B, C):
def who(self):
return "D->" + super().who()(a) Write out the Method Resolution Order (MRO) of class D as an ordered list of class names. (3 marks)
(b) State the exact string returned by D().who(). (4 marks)
(c) Explain in one or two sentences why B.who's call to super().who() resolves to C.who and not to A.who, referring to the C3 linearization. (3 marks)
Question 2 — Properties & Encapsulation (12 marks)
Design a class Temperature that stores a temperature internally in Celsius but exposes it through controlled access.
Requirements:
- The constructor accepts a Celsius value.
- A property
celsius(get/set) that rejects (raisesValueError) any set value below absolute zero, . - A property
fahrenheit(get/set) that reads/writes the same underlying state, using . - The internal Celsius attribute must be name-mangled (not directly accessible as a plain public name).
(a) Write the complete class. (9 marks)
(b) Given t = Temperature(25), state the value of t.fahrenheit, and describe what happens when t.fahrenheit = -500 is executed. (3 marks)
Question 3 — Operator Overloading (12 marks)
Implement a class Vector2D representing a 2D vector with attributes x and y, supporting:
__init__(self, x, y)__add__— vector addition returning a newVector2D.__eq__— equality based on both components.__lt__— comparison based on magnitude .__hash__— consistent with__eq__so instances can be dict keys.__repr__— returns a string likeVector2D(3, 4).
(a) Write the complete class. (9 marks)
(b) Explain why defining __eq__ without __hash__ would make instances unhashable, and why your __hash__ must be consistent with __eq__. (3 marks)
Question 4 — Abstract Base Classes & Polymorphism (10 marks)
You are building a billing system.
(a) Define an abstract base class PaymentMethod (using the abc module) with an abstract method pay(self, amount). (4 marks)
(b) Create two concrete subclasses CreditCard and Cash, each implementing pay to return a distinct descriptive string (e.g. include the amount). (4 marks)
(c) Write a single function checkout(method, amount) that works with any PaymentMethod without checking its concrete type, and name the OOP principle this demonstrates. (2 marks)
Question 5 — Dataclasses & Composition (6 marks)
(a) Using @dataclass, define an Engine with an integer field horsepower, and a Car that composes an Engine (has-a relationship) plus a string field model. (3 marks)
(b) Add a __post_init__ to Car that raises ValueError if the engine's horsepower is negative. State one difference between the composition used here and inheritance. (3 marks)
Answer keyMark scheme & solutions
Question 1 (10 marks)
(a) MRO (3 marks)
[D, B, C, A, object] — full 3 marks; deduct 1 per misplaced/missing class.
C3 linearization of D(B, C): D + merge(L[B], L[C], [B, C]) gives D, B, C, A, object.
(b) Return value (4 marks)
Following the MRO chain, each who prepends its label and calls the next class in the MRO:
D.who→"D->" + B.whoB.who→"B->" + C.who(next after B in MRO is C)C.who→"C->" + A.whoA.who→"A"
Result: "D->B->C->A" (4 marks; 2 if chain right but final string wrong).
(c) Why (3 marks)
super() does not mean "the parent class"; it means "the next class in the instance's MRO after the current one." Since the MRO of D places C immediately after B, super().who() inside B.who (when called on a D instance) dispatches to C.who. (Full 3 marks for referencing MRO-ordering, not literal parent.)
Question 2 (12 marks)
(a) Class (9 marks)
class Temperature:
def __init__(self, celsius):
self.celsius = celsius # goes through setter (validation) — 1
@property # getter — 1
def celsius(self):
return self.__celsius # name-mangled attr — 1
@celsius.setter # setter — 1
def celsius(self, value):
if value < -273.15: # validation — 2
raise ValueError("below absolute zero")
self.__celsius = value # store — 1
@property # fahrenheit getter — 1
def fahrenheit(self):
return self.__celsius * 9 / 5 + 32
@fahrenheit.setter # fahrenheit setter reuses celsius — 1
def fahrenheit(self, value):
self.celsius = (value - 32) * 5 / 9Mark breakdown: constructor via setter (1), celsius getter (1), setter with validation (3), name mangling __celsius (1), fahrenheit getter (1), fahrenheit setter routing through celsius for validation (2).
(b) (3 marks)
t.fahrenheit— (1)t.fahrenheit = -500computes celsius — (1)- This is below , so the
celsiussetter raisesValueError. — (1)
Question 3 (12 marks)
(a) Class (9 marks)
import math
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y # init — 1
def __add__(self, other): # add — 2
return Vector2D(self.x + other.x, self.y + other.y)
def __eq__(self, other): # eq — 2
return (self.x, self.y) == (other.x, other.y)
def _mag(self):
return math.hypot(self.x, self.y)
def __lt__(self, other): # lt — 2
return self._mag() < other._mag()
def __hash__(self): # hash — 1
return hash((self.x, self.y))
def __repr__(self): # repr — 1
return f"Vector2D({self.x}, {self.y})"(b) (3 marks)
- When you define
__eq__, Python sets__hash__toNoneautomatically, making instances unhashable (can't be dict keys / set members). (1.5) __hash__must be consistent with__eq__: equal objects must have equal hashes, otherwise dict/set lookups fail (an object could hash to a different bucket than its equal twin). Using the same(x, y)tuple for both guarantees this. (1.5)
Question 4 (10 marks)
(a) ABC (4 marks)
from abc import ABC, abstractmethod
class PaymentMethod(ABC): # inherits ABC — 2
@abstractmethod # abstractmethod — 2
def pay(self, amount):
...Note: attempting PaymentMethod() directly raises TypeError.
(b) Subclasses (4 marks, 2 each)
class CreditCard(PaymentMethod):
def pay(self, amount):
return f"Paid {amount} by credit card"
class Cash(PaymentMethod):
def pay(self, amount):
return f"Paid {amount} in cash"(c) (2 marks)
def checkout(method, amount):
return method.pay(amount) # no isinstance / type check — 1Principle: Polymorphism (duck typing — any object with a pay interface works). (1)
Question 5 (6 marks)
(a) (3 marks)
from dataclasses import dataclass, field
@dataclass
class Engine:
horsepower: int # 1
@dataclass
class Car:
engine: Engine # composition (has-a) — 1
model: str # 1(b) (3 marks)
def __post_init__(self):
if self.engine.horsepower < 0:
raise ValueError("horsepower cannot be negative")(__post_init__ correct — 2)
Difference (1): Composition means Car has-a Engine (delegates to a separate object it owns); inheritance would mean Car is-a subtype, gaining Engine's interface directly. Composition is more flexible/loosely coupled.
[
{"claim": "Temperature 25C equals 77F", "code": "C=25; F=C*Rational(9,5)+32; result = (F==77)"},
{"claim": "fahrenheit=-500 maps to celsius below -273.15", "code": "F=-500; C=(F-32)*Rational(5,9); result = (C < Rational(-27315,100))"},
{"claim": "Vector (1,2)+(3,4) = (4,6)", "code": "x=1+3; y=2+4; result = ((x,y)==(4,6))"},
{"claim": "magnitude of (3,4) is 5", "code": "m=sqrt(3**2+4**2); result = (m==5)"}
]