Designing and Writing Classes
Six problems. Four write classes, one refactors existing dictionary code into a class, and the last asks you to judge whether a class is warranted at all — sometimes it is not.
Contents
What this session is for
- Write classes with
__init__and methods comfortably - Keep instance and class variables straight
- See how encapsulation protects the validity of data
- Develop a sense of when a class is the wrong answer
Problems
9-1 Rectangle
Implement Rectangle:
class Rectangle:
def __init__(self, width: float, height: float) -> None: ...
def area(self) -> float: ...
def perimeter(self) -> float: ...
def is_square(self) -> bool: ...
def scale(self, factor: float) -> None: ... # scale in place
r = Rectangle(3, 4)
r.area() → 12
r.perimeter() → 14
r.is_square() → False
r.scale(2); r.area() → 48
Answer in your notes: did you make scale modify the object or return a new one, and why?
9-2 Stack
Implement Stack, last in first out (stacks came up in L7 with the call stack):
class Stack:
def __init__(self) -> None: ...
def push(self, item: int) -> None: ...
def pop(self) -> int: ...
def peek(self) -> int: ...
def is_empty(self) -> bool: ...
def size(self) -> int: ...
What should pop do on an empty stack? You have three options: return None, raise an exception, or require the caller to check first. All three are defensible — but pick one and justify it in your notes.
9-3 A counter
Implement WordCounter:
class WordCounter:
def __init__(self) -> None: ...
def add(self, word: str) -> None: ... # increment the count
def get(self, word: str) -> int: ... # 0 for unseen words
def total(self) -> int: ... # sum of all counts
def most_common(self, n: int) -> list[tuple[str, int]]: ...
most_common sorts as in Lab 6: most frequent first, ties broken alphabetically.
9-4 Temperature: let encapsulation hold the line
Implement Temperature, storing Celsius internally:
class Temperature:
def __init__(self, celsius: float) -> None: ...
def celsius(self) -> float: ...
def fahrenheit(self) -> float: ...
def kelvin(self) -> float: ...
def set_celsius(self, value: float) -> bool: ... # True on success
Requirement: any value below absolute zero (−273.15 °C) must be refused, returning False and leaving the stored value unchanged.
Store it in self._celsius, with the single underscore.
Answer in your notes: without a class — if everyone simply kept a celsius variable and did the arithmetic themselves — what would go wrong?
9-5 Refactor: from dictionaries to a class
Here is some working code. Refactor it into a Gradebook class.
def add_score(book, name, subject, score):
if name not in book:
book[name] = {}
book[name][subject] = score
def average(book, name):
scores = book[name].values()
return sum(scores) / len(scores)
def top_student(book):
return max(book, key=lambda n: average(book, n))
gb = {}
add_score(gb, "Alex", "maths", 87)
add_score(gb, "Alex", "physics", 92)
add_score(gb, "Blake", "maths", 65)
print(average(gb, "Alex"), top_student(gb))
The class should offer add(name, subject, score), average(name) and top_student().
Fix the two problems in the original along the way (hint: what happens when you ask about a student who is not there? what does top_student do with no students at all?). Say what you fixed in your notes.
9-6 Class or not?
For each of these four, decide whether a class is warranted and give a one-sentence reason:
- converting Celsius to Fahrenheit
- representing a deck of cards that can be shuffled, dealt from, and counted
- deciding whether a string is a palindrome
- a chatbot that remembers the conversation so far and replies accordingly
Before you submit
- Every class and method is annotated
- 9-2 picks one behavior for an empty pop and justifies it
- 9-3 tested with two objects to confirm the data does not leak
- 9-4 tested with
-300to confirm it is refused - 9-5 fixes both problems in the original and says so
- 9-6 gives a reason for all four, and not all four are “yes”
- AI usage declaration written