Stage 3 of 6 · Theory, REPL open · about 30 minutes
First class
Time to stand on the other side of the counter. You will build ReactionLog, a scorekeeper for the Week 5 reaction timer: pure Python, no hardware, runnable anywhere, and small enough to understand to the last character. It becomes part of this afternoon's build.
The goal: a scorekeeper
Week 5's stretch rung wanted best-of-five with a best and average report, and the loose-variable version was already awkward: a list here, a counter there, functions taking them all as parameters. Specify the capsule instead, Week 5 style, before writing it:
| ID | Requirement for ReactionLog |
|---|---|
| L1 | A new log shall start empty. |
| L2 | add(ms) shall record one reaction time. |
| L3 | count(), best() and average() shall report the number of times, the fastest, and the mean. |
| L4 | On an empty log, best() and average() shall return None rather than crash. |
The class statement, annotated
# the keyword, then the class name in CapWords (the PEP 8 convention)
class ReactionLog:
# the constructor: runs once, automatically, for each new instance.
# Its job: give the instance its starting state. L1 lives here.
def __init__(self):
self.times = []
# a method: an ordinary def, indented into the class,
# whose first parameter is always the instance it acts on
def add(self, ms):
self.times.append(ms)
class ReactionLog:opens a block, exactly asdefandifdo; everything indented under it belongs to the class. Names in CapWords (ReactionLog,NightLight) mark classes; lowercase marks instances and variables, per PEP 8, the style guide the whole Python world shares.__init__(two underscores each side, "dunder init") is the constructor: Python calls it for you whenever the class is called.log = ReactionLog()creates a blank instance, runs__init__on it, and hands it back. Attributes are born here:self.times = []gives this instance its own empty list.- Methods are defs inside the class, with the instance as first parameter. Nothing else about them is new: parameters, returns, bodies, all Week 4 material.
Use it exactly like a gpiozero class:
>>> log = ReactionLog()
>>> log.add(431)
>>> log.add(388)
>>> log.times
[431, 388]
The truth about self
self is the single biggest beginner hurdle in Python OOP, and one rewrite dissolves it. These two lines do the same thing:
log.add(431) # what you write
ReactionLog.add(log, 431) # what Python actually does
A method is a function that lives on the class; calling it through an instance makes Python pass that instance as the first argument. So inside add, self is log, the specific object being operated on, and self.times is that object's own list. Three consequences, each a classic error when forgotten (Troubleshoot has the exact messages):
- Every method's
deflistsselffirst, but no call ever passes it; Python supplies it. - Attributes are always reached through it:
self.times, never baretimes. Without the prefix, Python sees an ordinary (and undefined) local variable. selfis a convention, not a keyword; the first parameter could be named anything, and never is. Every Python programmer on Earth writesself, so you do too.
Aside: just enough lists
The five list facts this week needs
A list is an ordered collection, itself an object, from the tutorial's Data Structures chapter (read section 5.1 this week; the rest can wait). Today's working set: [] makes an empty one; times.append(x) adds to the end; len(times) counts; min(times) and sum(times) do what they say; for t in times: visits each item, the Week 4 for-loop pointed at your own data. That is everything ReactionLog requires.
Growing the class
L3, straight from the facts above, plus a constructor argument to name the player, showing how construction facts become attributes:
class ReactionLog:
def __init__(self, player):
self.player = player
self.times = []
def add(self, ms):
self.times.append(ms)
def count(self):
return len(self.times)
def best(self):
return min(self.times)
def average(self):
return sum(self.times) / len(self.times)
>>> log = ReactionLog("Sam")
>>> log.add(431); log.add(388); log.add(512)
>>> log.player, log.count(), log.best(), round(log.average())
('Sam', 3, 388, 444)
Notice what __init__(self, player) did to construction: ReactionLog() now fails with a missing-argument error, because the blueprint declares that a log cannot exist without a player name. Constructors are where a class states its non-negotiables, exactly as LED(17) refuses to exist without a pin.
The empty log: a spec question in code
Run ReactionLog("Sam").best() and Python answers with a crash: ValueError: min() iterable argument is empty. Week 5 trained you to hear that as a specification question, and L4 already answered it: return None. The guard is two lines per method:
def best(self):
if not self.times: # an empty list counts as False
return None
return min(self.times)
Same pattern for average(). Two things worth keeping: if not self.times: is the standard Pythonic emptiness test, and the deeper lesson, edge cases belong to the spec, and the class is where the spec's answer gets enforced once, for every future caller. That is encapsulation doing its job.
Many logs, one class
The payoff Week 5's loose variables could never give cheaply:
>>> sam, ana = ReactionLog("Sam"), ReactionLog("Ana")
>>> sam.add(431); ana.add(297)
>>> sam.count(), ana.count()
(1, 1)
>>> ana.best() < sam.best()
True
Each instance got its own times list because __init__ runs per instance and creates it fresh. One caution before the lab: attributes must be created inside __init__, not in the class body. A list written directly under class belongs to the blueprint itself and is shared by every instance, a legendary bug with its own row in Troubleshoot.
Tracing a method call
Week 5's trace tables work unchanged; the only new column is self. Trace ana.add(297) followed by ana.best():
| Step | Line | self is | self.times | Returns / output |
|---|---|---|---|---|
| 1 | ana.add(297) → add(self=ana, ms=297) | ana | [] | – |
| 2 | self.times.append(ms) | ana | [297] | – |
| 3 | ana.best() → best(self=ana) | ana | [297] | – |
| 4 | if not self.times: | ana | [297] | non-empty → skip |
| 5 | return min(self.times) | ana | [297] | 297 |
Note that sam.times never appears: the whole trace lives on the instance the calls went through. Expect exactly this exercise, on paper, on the midterm.
Checklist for this stage
Check yourself
When does __init__ run, and what is its one job?
Inside add, what exactly is self when the call was sam.add(431)?
sam. Python translated the call to ReactionLog.add(sam, 431), so self.times is Sam's list and no one else's.A teammate writes times.append(ms) inside a method and gets NameError: name 'times' is not defined. Why?
self. prefix, times is an ordinary local variable, and none exists. Attributes live on the instance and are always reached through self.Why did L4 (return None when empty) belong in the spec rather than being left to the coder?
What changes for callers when __init__(self) becomes __init__(self, player)?
ReactionLog() raises a missing-argument TypeError; ReactionLog("Sam") works and stores the name as an attribute. Constructor parameters are the facts an instance cannot exist without.Write the class header and constructor for a Door that starts closed.
class Door: then def __init__(self): containing self.is_open = False. Methods like open() and close() would flip that attribute, and nothing outside the class should touch it directly.