The Walrus Operator
Let’s suppose that we need to process a set of instructions to a robot. The instructions are in a file, one per line, and they are of two kinds:
A move instruction looks like this:
MOVE 7
This instructs the robot to move forwards by 7 units. The number of units will always be a whole, positive number.
A turn instruction looks like this:
TURN 90
This instructs the robot to turn counterclockwise by 90 degrees. The number of degrees can be a positive or negative whole number.
Our goal is to read a file containing such instructions and determine where the robot is after executing them. The robot starts at the origin (0,0), facing east (i.e. along to positive x-axis).
Let’s begin by writing a function that outputs such a list of instructions at random.
from random import seed, randint
seed()
def random_instructions(count=8, unit_max=8):
"""
Write a number of robot instructions given by count. The turns are in
multiples of 10 degrees.
"""
for i in range(count):
if 0 == randint(0, 1):
units = randint(1, unit_max)
print(f"MOVE {units:4d}")
else:
degrees = 10 * randint(-18, 18)
print(f"TURN {degrees:4d}")
Let’s try it out.
random_instructions()
TURN -80 MOVE 5 MOVE 7 TURN -30 TURN -180 MOVE 3 TURN 30 TURN 180
Now let’s write some instructions to a file.
from contextlib import redirect_stdout
INSTRUCTION_FILE = "/tmp/robot-instructions.txt"
with open(INSTRUCTION_FILE, "w") as rif:
with redirect_stdout(rif):
random_instructions(16)
Now we turn to reading the instructions.
def follow_instructions(filename):
with open(filename) as insts:
for line in insts.readlines():
print(line, end="")
follow_instructions(INSTRUCTION_FILE)
TURN -80 TURN 140 MOVE 7 TURN 0 TURN 130 MOVE 8 TURN -50 MOVE 3 MOVE 5 TURN 180 MOVE 8 MOVE 1 TURN 30 MOVE 4 MOVE 6 MOVE 5
Note how each line is read including its final newline. The print statement
then prints the line without adding another newline.
def follow_instructions(filename):
with open(filename) as insts:
for line in insts.readlines():
print(line, end="")
follow_instructions(INSTRUCTION_FILE)
TURN -80 TURN 140 MOVE 7 TURN 0 TURN 130 MOVE 8 TURN -50 MOVE 3 MOVE 5 TURN 180 MOVE 8 MOVE 1 TURN 30 MOVE 4 MOVE 6 MOVE 5
There are several ways we could now continue but we chose to use regular expressions so that we can demonstrate the walrus operator.
from re import compile, match
RE_MOVE = compile(r"MOVE +(\d+)")
RE_TURN = compile(r"TURN +(-?\d+)")
def follow_instructions(filename):
with open(filename) as insts:
for line in insts.readlines():
if m := match(RE_MOVE, line):
print("moving", m.group(1), "units")
elif m := match(RE_TURN, line):
print("turning", m.group(1), "degrees")
else:
print("Failed to parse:", line)
follow_instructions(INSTRUCTION_FILE)
turning -80 degrees turning 140 degrees moving 7 units turning 0 degrees turning 130 degrees moving 8 units turning -50 degrees moving 3 units moving 5 units turning 180 degrees moving 8 units moving 1 units turning 30 degrees moving 4 units moving 6 units moving 5 units
Notice how match(RE_MOVE, line) tries to match the MOVE regular expression
against line. If it fails it returns None and the if statement fails. However
if it succeeds it returns a match object which makes the if statement succeed.
The walrus operator := captures the result of the match in the variable m. This
can then be used in the body of the if statement.
Now let’s fill in the actual actions. Note that the trigonometric operations in
Python’s math library take their arguments in radians so we, write functions
that take their arguments in degrees.
from math import pi, cos, sin
def d2r(d):
return pi * d / 180
def cosd(d):
return cos(d2r(d))
def sind(d):
return sin(d2r(d))
def follow_instructions(filename, location=None, orientation=0):
if not location:
location = [0, 0]
with open(filename) as insts:
for line in insts.readlines():
if m := match(RE_MOVE, line):
units = int(m.group(1))
location[0] += units * cosd(orientation)
location[1] += units * sind(orientation)
print(f"MOVE {units:4d}")
elif m := match(RE_TURN, line):
degrees = int(m.group(1))
orientation += degrees
print(f"TURN {degrees:5d}")
else:
print("Failed to parse:", line)
print(f"{orientation:4d} ({location[0]:4.2f}, {location[1]:4.2f})")
print(location)
follow_instructions(INSTRUCTION_FILE)
TURN -80 -80 (0.00, 0.00) TURN 140 60 (0.00, 0.00) MOVE 7 60 (3.50, 6.06) TURN 0 60 (3.50, 6.06) TURN 130 190 (3.50, 6.06) MOVE 8 190 (-4.38, 4.67) TURN -50 140 (-4.38, 4.67) MOVE 3 140 (-6.68, 6.60) MOVE 5 140 (-10.51, 9.82) TURN 180 320 (-10.51, 9.82) MOVE 8 320 (-4.38, 4.67) MOVE 1 320 (-3.61, 4.03) TURN 30 350 (-3.61, 4.03) MOVE 4 350 (0.33, 3.34) MOVE 6 350 (6.24, 2.29) MOVE 5 350 (11.16, 1.43) [11.159698714204431, 1.4254821304651173]
If we want to check this more carefully we could write a set of tests but we can gain some confidence by making some simple sets of instructions.
def random_instructions(count=8, unit_max=8):
"""
Write a number of robot instructions given by count. The turns are in
multiples of 90 degrees.
"""
for i in range(count):
if 0 == randint(0, 1):
units = randint(1, unit_max)
print(f"MOVE {units:4d}")
else:
degrees = 90 * randint(-4, 4)
print(f"TURN {degrees:4d}")
with open(INSTRUCTION_FILE, "w") as rif:
with redirect_stdout(rif):
random_instructions(4)
follow_instructions(INSTRUCTION_FILE)
TURN 90 90 (0.00, 0.00) TURN -90 0 (0.00, 0.00) MOVE 7 0 (7.00, 0.00) MOVE 8 0 (15.00, 0.00) [15.0, 0.0]