Quiz: Object-Oriented Programming — Thinking in Objects
Test your understanding before moving on. Target: 70% or higher to proceed confidently.
Section 1: Multiple Choice (1 point each)
1. What is the relationship between a class and an object?
- A) A class is a variable; an object is a value
- B) A class is a blueprint; an object is an instance created from that blueprint
- C) A class is a function; an object is the return value
- D) A class and an object are the same thing
Answer
**B)** A class is a blueprint; an object is an instance created from that blueprint. *Why B:* A class defines the structure (attributes and methods). An object is a specific thing created from that class definition, with its own data. *Why not A:* Classes aren't variables — they're type definitions. *Why not C:* Classes contain functions (methods), but they aren't functions themselves. *Why not D:* They are fundamentally different — one is the template, the other is the product. *Reference:* Section 14.22. What does the __init__ method do?
- A) Destroys an object when it's no longer needed
- B) Initializes a new object's state when it's created
- C) Imports the class from a module
- D) Converts an object to a string
Answer
**B)** Initializes a new object's state when it's created. *Why B:* `__init__` is the constructor — it runs automatically when you create a new instance, setting up its initial attributes. *Why not A:* That would be `__del__` (the destructor), which is rarely used in Python. *Why not C:* Importing is handled by the `import` statement, not a class method. *Why not D:* That's `__str__`. *Reference:* Section 14.33. What does self refer to in a method?
- A) The class itself
- B) The current module
- C) The specific object the method is being called on
- D) The parent class
Answer
**C)** The specific object the method is being called on. *Why C:* When you call `alice.add_grade(92)`, Python passes `alice` as `self`. The method uses `self` to access that specific object's attributes. *Why not A:* `self` refers to the instance, not the class. The class is accessible via `type(self)` or the class name. *Why not B:* The module is a completely separate concept. *Why not D:* The parent class is accessed via `super()` ([Chapter 15](../chapter-15-inheritance-polymorphism/index.md)). *Reference:* Section 14.34. What happens when you run this code?
class Cat:
lives = 9
def __init__(self, name):
self.name = name
whiskers = Cat("Whiskers")
mittens = Cat("Mittens")
print(whiskers.lives, mittens.lives)
- A) Error —
livesis not defined - B)
9 9 - C)
None None - D)
0 0
Answer
**B)** `9 9` *Why B:* `lives` is a class attribute shared by all instances. Both `whiskers` and `mittens` can access it through the class. *Why not A:* Class attributes are accessible on instances — Python looks on the instance first, then the class. *Why not C/D:* The attribute is explicitly set to 9 at the class level. *Reference:* Section 14.45. What is the output of this code?
class Dog:
tricks = []
def __init__(self, name):
self.name = name
def learn(self, trick):
self.tricks.append(trick)
rex = Dog("Rex")
bella = Dog("Bella")
rex.learn("sit")
bella.learn("shake")
print(bella.tricks)
- A)
['shake'] - B)
['sit', 'shake'] - C)
[] - D) Error
Answer
**B)** `['sit', 'shake']` *Why B:* `tricks` is a class attribute — a single list shared by all instances. When Rex appends "sit" and Bella appends "shake", they're both modifying the same list. This is the "mutable class attribute" bug described in Section 14.9. *Why not A:* That's what you'd expect if `tricks` were an instance attribute, but it's not. *The fix:* Initialize `self.tricks = []` inside `__init__` to give each dog its own list. *Reference:* Section 14.9, Pitfall 26. What is the purpose of __str__?
- A) To create a new string object
- B) To define how the object is displayed when printed or converted to a string
- C) To compare the object to a string
- D) To initialize the object's string attributes
Answer
**B)** To define how the object is displayed when printed or converted to a string. *Why B:* `print(obj)` and `str(obj)` both call `obj.__str__()` to get a human-readable string representation. *Why not A:* `__str__` doesn't create new string objects in general — it returns a representation of the current object. *Why not C:* String comparison is handled by `__eq__`. *Why not D:* Initialization is handled by `__init__`. *Reference:* Section 14.67. What does the @property decorator do?
- A) Makes an attribute immutable
- B) Makes a method accessible like an attribute, with optional validation
- C) Makes an attribute private
- D) Creates a class attribute
Answer
**B)** Makes a method accessible like an attribute, with optional validation. *Why B:* `@property` lets you write `obj.x` instead of `obj.get_x()`, while still running code (validation, computation) behind the scenes. *Why not A:* You can define a setter with `@x.setter` to allow controlled mutation. *Why not C:* Privacy is handled by naming conventions (`_` and `__`), not `@property`. *Why not D:* `@property` defines instance-level behavior, not class attributes. *Reference:* Section 14.78. What naming convention signals "this attribute is internal, don't access it from outside"?
- A)
self.ATTRIBUTE - B)
self.attribute_ - C)
self._attribute - D)
self.attribute__
Answer
**C)** `self._attribute` *Why C:* A single leading underscore is the convention for "protected" — internal use, access at your own risk. *Why not A:* ALL_CAPS typically indicates a constant, not privacy. *Why not B:* A trailing underscore is used to avoid conflicts with Python keywords (e.g., `class_`). *Why not D:* This is not a standard convention. *Reference:* Section 14.79. What is name mangling in Python?
- A) Renaming a variable to avoid a syntax error
- B) Python automatically renames
__attrto_ClassName__attrto discourage external access - C) Converting variable names from camelCase to snake_case
- D) Obfuscating code to prevent reverse engineering
Answer
**B)** Python automatically renames `__attr` to `_ClassName__attr` to discourage external access. *Why B:* When you define `self.__balance`, Python internally stores it as `self._BankAccount__balance`. This makes accidental access harder but doesn't truly prevent it. *Why not A:* Name mangling is specific to double-underscore attributes in classes. *Why not C:* Naming conventions are a style choice, not an automatic Python feature. *Why not D:* Python doesn't do code obfuscation — name mangling is about communication, not security. *Reference:* Section 14.710. When should you prefer a procedural approach over OOP?
- A) When you have multiple related pieces of data that change together
- B) When you're writing utility functions or stateless data transformations
- C) When you need multiple instances of the same concept
- D) When state changes over time
Answer
**B)** When you're writing utility functions or stateless data transformations. *Why B:* Functions like `calculate_average()`, `parse_csv()`, or `convert_temperature()` don't need to maintain state — they take input and produce output. A class would be overkill. *Why not A/C/D:* These are all signals that OOP would be a good fit — related data, multiple instances, and state management are core OOP strengths. *Reference:* Section 14.8Section 2: Code Reading (2 points each)
11. What is the output of this code?
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
def __eq__(self, other):
return self.x == other.x and self.y == other.y
p1 = Point(3, 4)
p2 = Point(3, 4)
p3 = p1
print(p1 == p2)
print(p1 is p2)
print(p1 is p3)
Answer
True
False
True
`p1 == p2` is `True` because `__eq__` compares x and y values, and both are (3, 4).
`p1 is p2` is `False` because `is` checks identity (same object in memory), and `p1` and `p2` are two different objects.
`p1 is p3` is `True` because `p3 = p1` makes both names point to the same object.
*Reference:* Section 14.6
12. What is the output of this code?
class Tracker:
count = 0
def __init__(self, label):
self.label = label
Tracker.count += 1
def __str__(self):
return f"{self.label} (#{Tracker.count})"
a = Tracker("Alpha")
b = Tracker("Beta")
c = Tracker("Gamma")
print(a)
print(Tracker.count)
Answer
Alpha (#3)
3
By the time `print(a)` runs, all three `Tracker` objects have been created, so `Tracker.count` is 3. The `__str__` method reads the class attribute at the time it's called, not at the time the object was created. All three objects share the same `Tracker.count`.
*Reference:* Section 14.4
13. What error does this code produce and why?
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def bigger_than(other):
return self.area() > other.area()
c1 = Circle(5)
c2 = Circle(3)
c1.bigger_than(c2)
Answer
TypeError: Circle.bigger_than() takes 1 positional argument but 2 were given
The `bigger_than` method is missing `self` as its first parameter. When `c1.bigger_than(c2)` is called, Python passes `c1` as the first argument and `c2` as the second. But the method only has one parameter (`other`), so `c1` gets assigned to `other` and `c2` has nowhere to go. The fix: `def bigger_than(self, other):`.
*Reference:* Section 14.9, Pitfall 1
14. What is the output of this code?
class Inventory:
def __init__(self):
self._items = {}
def add(self, item, quantity=1):
self._items[item] = self._items.get(item, 0) + quantity
def __len__(self):
return sum(self._items.values())
def __str__(self):
parts = [f"{item}: {qty}" for item, qty in self._items.items()]
return "Inventory(" + ", ".join(parts) + ")"
inv = Inventory()
inv.add("sword")
inv.add("potion", 3)
inv.add("potion", 2)
print(inv)
print(f"Total items: {len(inv)}")
Answer
Inventory(sword: 1, potion: 5)
Total items: 6
The `add` method uses `dict.get()` to safely add to existing quantities. "potion" is added twice (3 + 2 = 5). `__len__` returns the sum of all quantities (1 + 5 = 6), not the number of unique items.
*Reference:* Sections 14.5, 14.6
Section 3: Short Answer (2 points each)
15. A student writes this class and is confused by the output. Explain the bug and provide the fix.
class Team:
members = []
def __init__(self, name):
self.name = name
def add_member(self, person):
self.members.append(person)
red = Team("Red")
blue = Team("Blue")
red.add_member("Alice")
blue.add_member("Bob")
print(f"Red: {red.members}") # Expected: ['Alice']
print(f"Blue: {blue.members}") # Expected: ['Bob']
# Actual: both show ['Alice', 'Bob']
Answer
**Bug:** `members = []` is a class attribute — a single list shared by all `Team` instances. When `red.add_member("Alice")` appends to it, the same list is visible through `blue.members`. **Fix:** Move the list initialization into `__init__` to make it an instance attribute:class Team:
def __init__(self, name):
self.name = name
self.members = [] # Each team gets its own list
*Reference:* Section 14.9, Pitfall 2
16. Explain why the following __eq__ method is incomplete:
def __eq__(self, other):
return self.name == other.name
What problem could arise, and how should it be fixed?
Answer
**Problem:** If `other` is not the same type, calling `other.name` will raise an `AttributeError`. For example, comparing a `Student` with the string `"Alice"` would crash. **Fix:** Check the type first and return `NotImplemented` for unrecognized types:def __eq__(self, other):
if not isinstance(other, Student):
return NotImplemented
return self.name == other.name
Returning `NotImplemented` (not raising it) tells Python to try the comparison the other way around, rather than crashing.
*Reference:* Section 14.6
17. What is the difference between __str__ and __repr__? Write both for a Color class that stores r, g, b values.
Answer
`__str__` is for end users — called by `print()` and `str()`. It should be human-readable. `__repr__` is for developers — called in the REPL and by `repr()`. It should ideally be a valid Python expression that could recreate the object.class Color:
def __init__(self, r, g, b):
self.r = r
self.g = g
self.b = b
def __str__(self):
return f"rgb({self.r}, {self.g}, {self.b})"
def __repr__(self):
return f"Color({self.r}, {self.g}, {self.b})"
`str(Color(255, 128, 0))` returns `"rgb(255, 128, 0)"`.
`repr(Color(255, 128, 0))` returns `"Color(255, 128, 0)"`.
*Reference:* Section 14.6
Section 4: Design Questions (3 points each)
18. You're building a library management system. List the attributes and methods you would give a Book class and a Library class. Include at least 4 attributes and 4 methods for each.
Answer
**Book class:** - Attributes: `title`, `author`, `isbn`, `checked_out` (bool), `due_date`, `genre` - Methods: `check_out(due_date)`, `return_book()`, `is_overdue()`, `__str__()`, `__eq__()` (by ISBN), `__repr__()` **Library class:** - Attributes: `name`, `books` (list of Book objects), `patrons` (list or dict) - Methods: `add_book(book)`, `remove_book(isbn)`, `search(keyword)`, `check_out_book(isbn, patron, due_date)`, `return_book(isbn)`, `get_overdue()`, `__len__()`, `__str__()` The key insight is that the Library doesn't just store books — it manages the operations on the collection. The Book manages its own state (checked out, due date), and the Library coordinates the collection-level operations. *Reference:* Section 14.819. A colleague writes a single GameManager class with 40 methods that handles player movement, combat, inventory, saving/loading, rendering, and sound. What OOP principle are they violating, and how would you refactor it?
Answer
They've created a **god class** — a class that tries to do everything, violating the single responsibility principle. Refactoring: split into focused classes, each with one clear responsibility: - `Player` — movement, health, stats - `CombatSystem` — damage calculation, targeting - `Inventory` — item management, equipment - `SaveManager` — save/load game state - `Renderer` — display/graphics - `AudioManager` — sound effects, music Each class handles one concern. `GameManager` becomes a coordinator that delegates to these specialized classes rather than doing everything itself. *Reference:* Section 14.9, Pitfall 320. Consider this scenario: you need to process a list of student records (name, grades, major) to calculate class averages and produce a summary report. Would you use OOP, a procedural approach, or a mix? Justify your answer by listing the pros and cons for this specific problem.
Answer
**Best approach: A mix.** **OOP for Student records:** - Pros: Each student naturally bundles name, grades, and major. Methods like `calculate_gpa()` belong with the data. If you process thousands of students, each object manages its own state cleanly. **Procedural for the report generation:** - Pros: Calculating class averages and producing a report are one-time operations on a collection. A function like `generate_report(students)` is simpler than a `ReportGenerator` class for a one-off task. **The hybrid:**class Student:
# ... attributes and methods per student
def calculate_class_average(students):
# standalone function — no state to maintain
return sum(s.gpa for s in students) / len(students)
def generate_report(students, output_path):
# standalone function — input-to-output transformation
...
Using a class for each student and standalone functions for aggregate operations gives you the benefits of both paradigms without forcing everything into one model.
*Reference:* Section 14.8