CellIn[3], line 9case RED:
^
SyntaxError: name capture 'RED' makes remaining patterns unreachable
This generates a confusing error
This is because match is for structural matching
simple variable names are capture patterns
If we have the same match above, but with only the RED branch and pass GREEN:
def truncated_action(light):
match light:
case RED:
print(f"{RED=}, {light=}: Stop")
truncated_action(GREEN)
Naively we would expect this to resolve to case "red" != "green"
However, the first case is executed
We can see from the debug output above RED was reassigned the value of light, here GREEN
match is best thought of as analogous to variable unpacking
Tries to match the structure of the received argument
Python here is checking that the multiple assignment below, executes without error
(RED, ) = (light, )
We could write this in longform
def take_unpacking_action(light):try: (RED,) = (light,)exceptTypeError:# did not matchraiseRuntimeErrorelse:# Matchedprint(f"{RED=}, {light=}")take_unpacking_action(GREEN)
RED='green', light='green'
This explains why we couldn’t have our switch like style work
The YELLOW and GREEN cases have the same structure as the initial RED
Python determines that these cases are thus unreachable and throws a syntax error
A workaround is to ensure the case label has a . operator
Causes python to do an attribute look up
This means to emulate standard switch behaviour we can use an Enum
import enumclass ColourEnum(enum.StrEnum): RED ="red" YELLOW ="yellow" GREEN ="green"def take_enum_action(light):match light:case ColourEnum.RED:print("Stop")case ColourEnum.YELLOW:print("Slow")case ColourEnum.GREEN:print("Go")case _:raiseRuntimeErrortake_enum_action("red")take_enum_action("yellow")take_enum_action("green")
Stop
Slow
Go
The code works as expected
But we’ve had to add a whole load of boilerplate to get it to work as intended
match is for Destructuring
Destructuring means extracting components from a complex nested data structure
multiple assignment and tuple unpacking is an example of destructuring, e.g.
for index, value inenumerate("abc"):print(f"Index is {index} and value is {value}")
Index is 0 and value is a
Index is 1 and value is b
Index is 2 and value is c
We’ve seen that unpacking for lists and tuples can be used on deeply nested constructs too
match emulates this for dictionaries, sets and user-defined classes
This is restricted purely to determining control flow
Structural pattern matching is useful for dealing with heterogeneous objects
Or semi-structured data
Similar concepts are algebraic data types, sum types, tagged unions
Let’s look at using structural matching to help us search for a value in a binary tree
Tree is represented as a 3-tuple, indexed as,
The stored value
Left child (lower-valued)
Right child (higher-valued)
We use None to represent a missing child
A leaf node is represented by an inlined value rather than (value, None, None)
A nested tree containing \(7, 9, 10, 11, 13\) might then be defined as
my_tree = (10, (7, None, 9), (13, 11, None))
A recursive search using the if...elif...else can be written as below
def contains(tree, value):ifnotisinstance(tree, tuple):return tree == value pivot, left, right = treeif value < pivot:return contains(left, value)elif value > pivot:return contains(right, value)else:return value == pivotassert contains(my_tree, 9)assertnot contains(my_tree, 14)
We could instead implement this using match
my_tree = (10, (7, None, 9), (13, 11, None))def contains_match(tree, value):match tree:case pivot, left, _ if value < pivot:return contains_match(left, value)case pivot, _, right if value > pivot:return contains_match(right, value) case (pivot, _, _) | pivot:return pivot == valueassert contains_match(my_tree, 9)assertnot contains_match(my_tree, 14)
Eliminates the call to isinstance relies on the implicit unpacking
Code structure is regularised
Logic is more compact and inline
How does this work?
Each clause tries to extract the contents of tree to the given pattern
If the structure matches, must then pass any subsequent if statements
if clauses are sometimes called guard expressions
Statements in the case block are only evaluated if the guard expression is True
If the statement fails to match, we fall through to the next case
The pipe operator | is used to combine patterns
Means that either pattern must match
The second capture pattern is simple variable assignment so will catch anything
Used to capture leaf nodes
Now suppose we want to use class structure to capture our nodes
class Node:def__init__(self, value, left=None, right=None):self.value = valueself.left = leftself.right = right
We can use match to convert the JSON data to the appropriate class structure
import jsondef deserialize(data): record = json.loads(data)match record: case {"customer": {"last": last_name, "first": first_name}}:return PersonCustomer(first_name, last_name) case {"customer": {"entity": company_name}}:return BusinessCustomer(company_name)case _:raiseValueError("Unknown record type")print("Record for a person: ", deserialize(record_person))print("Record for a business: ", deserialize(record_business))
Record for a person: PersonCustomer(first_name='Bob', last_name='Ross')
Record for a business: BusinessCustomer(company_name="Steve's Painting Co.")