OOP Fundamentals
Level: 3 — Production (from-scratch derivations, code-from-memory, explain-out-loud) Time Limit: 45 minutes Total Marks: 60
Write all code from memory in Python 3. Where an "explain" is requested, answer in prose — clarity of reasoning is marked.
Question 1 — MRO & C3 Linearization (12 marks)
Consider this diamond hierarchy:
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass(a) Derive the Method Resolution Order of D by hand using the C3 linearization algorithm. Show the merge steps explicitly. (6)
(b) Explain why Python cannot produce a consistent MRO for the following, and name the error raised:
class X: pass
class Y(X): pass
class Z(X, Y): pass(3)
(c) State the two ordering constraints (monotonicity properties) that C3 guarantees. (3)
Question 2 — Class vs Instance Attributes (10 marks)
(a) Predict the exact printed output of the following and explain each line: (6)
class Counter:
total = 0
def __init__(self):
self.count = 0
Counter.total += 1
def tick(self):
self.count += 1
a = Counter()
b = Counter()
a.tick(); a.tick()
b.tick()
print(a.count, b.count, Counter.total)
a.total = 99
print(Counter.total, b.total)(b) Explain the "mutable class attribute" trap: why does using a list as a default class attribute shared across instances cause bugs? Give a one-line fix. (4)
Question 3 — Encapsulation & Properties (code from memory) (12 marks)
Write a class Temperature from scratch that:
- Stores temperature internally in Celsius using a name-mangled attribute
__celsius. (2) - Exposes a
celsiusproperty with a getter and a setter; the setter raisesValueErrorif the value is below absolute zero (−273.15). (4) - Exposes a read-only
fahrenheitproperty computed from Celsius (no setter). (3) - Includes a
__repr__returningTemperature(celsius=<value>). (2)
(b) Explain what name mangling does to __celsius and from what it actually protects the attribute. (1)
Question 4 — Operator Overloading (12 marks)
Implement a Vector2D class supporting __init__(x, y), __add__, __eq__, and __repr__. Then answer:
(a) Write the full class. __add__ returns a new Vector2D; __eq__ compares componentwise; __repr__ gives Vector2D(x, y). (6)
(b) You define __eq__. Python then sets __hash__ to None, making instances unhashable. Explain why Python does this and how you would restore hashability correctly. (3)
(c) Explain the contract between __eq__ and __hash__: state the rule that must hold for two objects. (3)
Question 5 — Polymorphism, ABCs & super() (14 marks)
(a) Write an abstract base class Shape using the abc module with an @abstractmethod area(). Then write Circle(radius) and Rectangle(w, h) subclasses. Show that instantiating Shape() directly fails. (6)
(b) Write a function total_area(shapes) that sums area() across a mixed list. Explain how this demonstrates duck typing / polymorphism. (4)
(c) Given:
class Base:
def __init__(self, name):
self.name = name
class Logged(Base):
def __init__(self, name):
super().__init__(name)
self.log = []Explain precisely what super().__init__(name) does here and why calling Base.__init__(self, name) directly would be inferior in a multiple-inheritance setting. (4)
Answer keyMark scheme & solutions
Question 1 (12)
(a) C3 linearization of D (6 marks)
Notation: L[X] = linearization of X. merge takes heads while a head appears in no tail.
L[A] = [A, object]L[B] = [B] + merge(L[A], [A]) = [B, A, object]L[C] = [C, A, object]L[D] = [D] + merge(L[B], L[C], [B, C])
Merge steps:
merge([B,A,object], [C,A,object], [B,C])
take B (head of list1, not in any tail) → B
merge([A,object], [C,A,object], [C])
A is in tail of list2 → skip; take C → C
merge([A,object], [A,object], [])
take A → A
take object → object
Result: L[D] = [D, B, C, A, object] (2 for correct L[B],L[C]; 3 for correct merge; 1 final answer)
(b) (3 marks) Z(X, Y) lists X before Y, but Y is a subclass of X, so Y must precede X in any consistent order. These constraints contradict each other, so C3 merge fails — no head can be selected without violating monotonicity. Python raises TypeError: Cannot create a consistent method resolution order (MRO). (2 explanation, 1 error name)
(c) (3 marks)
- Preserves local precedence order — the order bases are listed in a class definition is respected. (1.5)
- Monotonicity — if a class precedes another in one class's MRO, it does so in every subclass's MRO. (1.5)
Question 2 (10)
(a) (6 marks) Output:
2 1 2
2 99
a.count=2(ticked twice),b.count=1(once) — instance attributes are independent. (2)Counter.total=2— class attribute incremented once per__init__. (1)a.total = 99creates a new instance attribute shadowing the class attribute; it does not modifyCounter.total. (1.5)- Therefore
Counter.totalstays2, andb.totalstill reads the class attribute2. (1.5)
(b) (4 marks) A mutable class attribute (e.g. items = []) is a single object shared by all instances. Mutating it via self.items.append(...) (which resolves to the class object since no instance attribute exists) affects every instance. (2)
Fix: assign a fresh mutable in __init__:
def __init__(self):
self.items = [](2)
Question 3 (12)
(a) (11 marks)
class Temperature:
def __init__(self, celsius=0.0):
self.celsius = celsius # goes through setter
@property
def celsius(self): # getter (2)
return self.__celsius
@celsius.setter
def celsius(self, value): # setter (4)
if value < -273.15:
raise ValueError("below absolute zero")
self.__celsius = value
@property
def fahrenheit(self): # read-only (3)
return self.__celsius * 9 / 5 + 32
def __repr__(self): # (2)
return f"Temperature(celsius={self.__celsius})"Marks: name-mangled storage __celsius (2), getter+setter with validation (4), read-only fahrenheit (3), __repr__ (2).
(b) (1 mark) Name mangling rewrites __celsius to _Temperature__celsius, preventing accidental name clashes in subclasses. It is not true privacy — it can still be accessed via the mangled name. (1)
Question 4 (12)
(a) (6 marks)
class Vector2D:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
def __eq__(self, other):
if not isinstance(other, Vector2D):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
def __repr__(self):
return f"Vector2D({self.x}, {self.y})"Marks: __init__ (1), __add__ returns new object (2), __eq__ componentwise + type guard (2), __repr__ (1).
(b) (3 marks) When you define __eq__, Python sets __hash__ = None because the default identity-based hash would be inconsistent with your value-based equality (equal objects must hash equal). (2) Restore by defining __hash__ over the same fields used in __eq__:
def __hash__(self):
return hash((self.x, self.y))(1)
(c) (3 marks) Contract: if a == b then hash(a) == hash(b). (2) The converse need not hold (hash collisions are allowed), and unequal objects may share a hash. (1)
Question 5 (14)
(a) (6 marks)
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self): ...
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.hShape() raises TypeError: Can't instantiate abstract class Shape with abstract method area. (ABC+abstractmethod 2, Circle 1.5, Rectangle 1.5, instantiation failure 1)
(b) (4 marks)
def total_area(shapes):
return sum(s.area() for s in shapes)(2) Duck typing: total_area never checks the concrete type — it only requires each object to respond to area(). Any object with that interface works, so behavior varies by type while the calling code stays uniform (polymorphism). (2)
(c) (4 marks) super().__init__(name) looks up the next class in Logged's MRO (here Base) and calls its __init__, binding self automatically. (2) Hardcoding Base.__init__(self, name) bypasses the MRO: in a multiple-inheritance / diamond scenario it may skip cooperative __init__s of sibling classes, causing some bases to be initialized twice or not at all. super() follows the linearized chain cooperatively. (2)
[
{"claim":"C3 MRO of D is [D,B,C,A,object]","code":"class A: pass\nclass B(A): pass\nclass C(A): pass\nclass D(B,C): pass\nnames=[c.__name__ for c in D.__mro__]\nresult = names==['D','B','C','A','object']"},
{"claim":"Q2 output: a.count=2,b.count=1,total=2 then unchanged 2,2","code":"class Counter:\n total=0\n def __init__(self):\n self.count=0\n Counter.total+=1\n def tick(self):\n self.count+=1\na=Counter(); b=Counter()\na.tick(); a.tick(); b.tick()\nfirst=(a.count,b.count,Counter.total)\na.total=99\nsecond=(Counter.total,b.total)\nresult = first==(2,1,2) and second==(2,2)"},
{"claim":"Vector2D addition and equality work","code":"class Vector2D:\n def __init__(self,x,y): self.x,self.y=x,y\n def __add__(self,o): return Vector2D(self.x+o.x,self.y+o.y)\n def __eq__(self,o): return (self.x,self.y)==(o.x,o.y)\nresult = (Vector2D(1,2)+Vector2D(3,4))==Vector2D(4,6)"},
{"claim":"Fahrenheit conversion 100C -> 212F","code":"result = (100*9/5+32)==212"},
{"claim":"Instantiating ABC with abstractmethod raises TypeError","code":"from abc import ABC,abstractmethod\nclass Shape(ABC):\n @abstractmethod\n def area(self): ...\ntry:\n Shape(); result=False\nexcept TypeError:\n result=True"}
]