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()
MOVE 8 MOVE 1 MOVE 4 MOVE 6 MOVE 6 MOVE 8 TURN 120 MOVE 2
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) follow_instructions(INSTRUCTION_FILE)
TURN -130 MOVE 3 TURN 110 TURN 180 TURN 30 TURN -20 MOVE 7 MOVE 2 TURN 100 TURN 60 TURN 80 MOVE 6 MOVE 2 TURN 170 MOVE 1 MOVE 4
Note how each line is read including its final newline. The print statement then prints the line including its newline and then adds another newline. We can suppress this by telling the print statement how to end its lines.
def follow_instructions(filename): with open(filename) as insts: for line in insts.readlines(): print(line, end="") follow_instructions(INSTRUCTION_FILE)
TURN -130 MOVE 3 TURN 110 TURN 180 TURN 30 TURN -20 MOVE 7 MOVE 2 TURN 100 TURN 60 TURN 80 MOVE 6 MOVE 2 TURN 170 MOVE 1 MOVE 4
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 -130 degrees moving 3 units turning 110 degrees turning 180 degrees turning 30 degrees turning -20 degrees moving 7 units moving 2 units turning 100 degrees turning 60 degrees turning 80 degrees moving 6 units moving 2 units turning 170 degrees moving 1 units moving 4 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})") return location follow_instructions(INSTRUCTION_FILE)
TURN -130 -130 (0.00, 0.00) MOVE 3 -130 (-1.93, -2.30) TURN 110 -20 (-1.93, -2.30) TURN 180 160 (-1.93, -2.30) TURN 30 190 (-1.93, -2.30) TURN -20 170 (-1.93, -2.30) MOVE 7 170 (-8.82, -1.08) MOVE 2 170 (-10.79, -0.74) TURN 100 270 (-10.79, -0.74) TURN 60 330 (-10.79, -0.74) TURN 80 410 (-10.79, -0.74) MOVE 6 410 (-6.93, 3.86) MOVE 2 410 (-5.65, 5.39) TURN 170 580 (-5.65, 5.39) MOVE 1 580 (-6.42, 4.75) MOVE 4 580 (-9.48, 2.18)
[-9.47955394427207, 2.179117766164568]
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 -360 -360 (0.00, 0.00) MOVE 6 -360 (6.00, 0.00) TURN -90 -450 (6.00, 0.00) TURN 270 -180 (6.00, 0.00) [6.0, 1.4695761589768238e-15]