Chapter 4 · Objects and robust code
Dataclasses, enums and match
Step 1 of 21 · Concept
@dataclass writes __init__, __repr__ and __eq__
By the end of this lesson, you can write records, named choices and decisions with less code.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
@dataclassis a decorator: it changes the class under it.- Each field is a name and its type,
x: int. From the fields it writes__init__,__repr__and__eq__.
Step 2 of 21 · Quiz
Predict the points
Type exactly what this program prints.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p)
print(p == Point(1, 2))
print([p, Point(0, 0)])
Step 3 of 21 · Demo
See it run
Run it and compare with your answer. Importing dataclasses takes Python about a second here.
Step 4 of 21 · Concept
Type hints say what you mean
The int in x: int is a type hint. A function can have them too:
def area(w: float, h: float) -> float:
return w * h
w: float says w should be a float. -> float says what the function returns.
Python does not check them when the program runs. They are notes for people and for tools that read code.
Step 5 of 21 · Quiz
Does it run?
The hints say int, but the call passes text. What happens?
def double(n: int) -> int:
return n * 2
print(double("ab"))
Step 6 of 21 · Concept
Fields can have defaults; records can be frozen
@dataclass
class Item:
name: str
price: float
qty: int = 1
qty: int = 1gives a field a default:Item("pen", 1.5)has aqtyof 1. Fields with defaults come last.@dataclass(frozen=True)writes a record that cannot change. Setting a field raisesFrozenInstanceError, a kind ofAttributeError.
Step 7 of 21 · Demo
Defaults and frozen records
Run it. The first item uses the default; the frozen Point refuses a new x.
Step 8 of 21 · Exercise
Your turn: an Item record
- Write the dataclass
Item:name,price, andqtywith a default of 1. - Give it a method
total()that returns the price times the quantity.
The program must print:
Item(name='pen', price=1.5, qty=1)
pen: 1.50
cup: 9.00
Defaults and frozen records has the fields. A dataclass is still a class, so a method goes in it as in lesson 15.
Under the fields, def total(self): returns self.price times self.qty.
return self.price * self.qty
Step 9 of 21 · Concept
An Enum names a fixed set of choices
from enum import Enum
class Light(Enum):
RED = 1
YELLOW = 2
GREEN = 3
Light.REDis one member of the enum.printshows it asLight.RED..namegives"RED", and.valuegives1.for light in Light:goes through the members in order. Compare one with==:light == Light.RED.
A misspelt member, such as Light.RDE, raises AttributeError when that line runs. A misspelt text such as
"rde" raises nothing.
Step 10 of 21 · Quiz
Predict the light
Type exactly what this program prints.
from enum import Enum
class Light(Enum):
RED = 1
YELLOW = 2
GREEN = 3
light = Light.YELLOW
print(light)
print(light.name, light.value)
print(light == Light.YELLOW, light == Light.RED)
for light in Light:
print(light.name)Step 11 of 21 · Demo
See it run
Run it and compare with your answer.
Step 12 of 21 · Exercise
Your turn: the next light
Finish next_light(light) with if and elif: after RED comes GREEN, after GREEN comes YELLOW, after
YELLOW comes RED. The program must print:
RED
GREEN
YELLOW
RED
The function gets one member and returns another member: Light.GREEN, not the text "GREEN".
A chain: if light == Light.RED: returns Light.GREEN, an elif handles GREEN, and else is YELLOW's.
if light == Light.RED:
return Light.GREEN
Step 13 of 21 · Concept
match picks the first case that fits
match command:
case "go":
print("Going")
case "stop" | "halt":
print("Stopping")
case _:
print("Unknown:", command)
- Python tries the cases from the top and runs the first that fits, as an
elifchain does. |means "or"._fits anything, so the last case catches the rest.
Step 14 of 21 · Quiz
Predict the replies
Type exactly what this program prints.
for command in ["go", "halt", "jump"]:
match command:
case "go":
print("Going")
case "stop" | "halt":
print("Stopping")
case _:
print("Unknown:", command)
Step 15 of 21 · Demo
See it run
Run it and compare with your answer.
Step 16 of 21 · Concept
Patterns take things apart
A pattern can also take a value apart and name its pieces:
| case | fits | names |
|---|---|---|
["move", steps] | a list of two, the first "move" | steps: the second item (text, from split()) |
Point(x=0, y=y) | a Point whose x is 0 | y: its y |
Light.RED | that member: a dotted name is a value | nothing |
A bare name in a case captures anything. So case RED: fits every light, and when more cases follow it, Python
refuses the file: SyntaxError: name capture 'RED' makes remaining patterns unreachable. Write case Light.RED:.
Step 17 of 21 · Demo
A robot reads its orders
Run it. Each order is split into a list and matched; int(n) turns the captured text into a number.
"report".split() is ["report"], a list of one item, so case ["report"]: fits it.
Step 18 of 21 · Exercise
Your turn: match the lights
Write next_light again, with match this time: one case per member. The case for Light.RED is there; add
the other two. The program must print:
RED
GREEN
YELLOW
RED
Each case is a value pattern, a member with its class in front, as case Light.RED: is. It returns the next
member.
Write case Light.RED:, never case RED:. A bare name captures every light, and Python refuses the file with
SyntaxError: name capture 'RED' makes remaining patterns unreachable.
case Light.GREEN:
return Light.YELLOW
Step 19 of 21 · Check yourself
Check yourself: a tiny robot
No shape this time.
- Write a dataclass
Positionwithxandy, both 0 by default. - For each order,
up 3adds 3 toyandright 2adds 2 tox. reportprints the position; any other order printsUnknown:and the order.
The program must print:
Position(x=2, y=4)
Unknown: fly 9
A robot reads its orders is the model: one match on the split order inside the loop. Here the position can
change, so add to its fields in place.
Step 20 of 21 · Challenge · optional
Challenge: only big moves
Optional.
Moves over 10 steps are big. Make the program print:
big move 12
move 3
big move 40
A case can end with a guard, if and a condition, before its :. It fits only when the pattern fits and the
condition is True. Put the big case first.
n is text. A guard if n > 10 stops with TypeError: '>' not supported between instances of 'str' and 'int'.
Convert it first.
case ["move", n] if int(n) > 10:
Step 21 of 21 · Recap
Recap
@dataclasswrites__init__,__repr__and__eq__from annotated fields.frozen=Trueforbids changes.- Type hints such as
w: floatare notes. Python does not check them when the program runs. - An
Enumnames a fixed set of choices:Light.RED,.name,.value. matchruns the first case that fits: literals,|,_, lists, classes and dotted values.
Next: lesson 20 keeps data after the program ends: files.
Review
4 questions from this lesson, in a new order each time. Answer from memory: 3 of 4 right on the first try passes. On a day after you finish the lesson, a pass earns 15 XP.
The review needs JavaScript.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p)
print(p == Point(1, 2))
print([p, Point(0, 0)])
from dataclasses import dataclass
@dataclass
class Item:
name: str
price: float
qty: int = 1
print(Item("pen", 1.5))
print(Item("cup", 2.25, 4))
@dataclass(frozen=True)
class Point:
x: int
y: int
origin = Point(0, 0)
try:
origin.x = 5
except AttributeError:
print("A frozen Point cannot change")
from dataclasses import dataclass
# write the dataclass Item here
cart = [Item("pen", 1.5), Item("cup", 2.25, 4)]
print(cart[0])
for item in cart:
print(f"{item.name}: {item.total():.2f}")
from enum import Enum
class Light(Enum):
RED = 1
YELLOW = 2
GREEN = 3
light = Light.YELLOW
print(light)
print(light.name, light.value)
print(light == Light.YELLOW, light == Light.RED)
for light in Light:
print(light.name)
from enum import Enum
class Light(Enum):
RED = 1
YELLOW = 2
GREEN = 3
def next_light(light):
return light
light = Light.RED
for i in range(4):
print(light.name)
light = next_light(light)
for command in ["go", "halt", "jump"]:
match command:
case "go":
print("Going")
case "stop" | "halt":
print("Stopping")
case _:
print("Unknown:", command)
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def where(p):
match p:
case Point(x=0, y=y):
return f"on the y axis at {y}"
case _:
return f"at {p.x}, {p.y}"
pos = Point(0, 0)
for order in ["up 3", "report", "right 2", "report", "jump"]:
match order.split():
case ["up", n]:
pos = Point(pos.x, pos.y + int(n))
case ["right", n]:
pos = Point(pos.x + int(n), pos.y)
case ["report"]:
print(where(pos))
case _:
print("Unknown:", order)
from enum import Enum
class Light(Enum):
RED = 1
YELLOW = 2
GREEN = 3
def next_light(light):
match light:
case Light.RED:
return Light.GREEN
case _:
return light
light = Light.RED
for i in range(4):
print(light.name)
light = next_light(light)
from dataclasses import dataclass
orders = ["up 3", "right 2", "up 1", "report", "fly 9"]
for order in ["move 12", "move 3", "move 40"]:
match order.split():
case ["move", n]:
print("move", n)