The Walrus Operator

500px-Noaa-walrus22.jpg

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:

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]

Author: Breanndán Ó Nualláin <o@uva.nl>

Date: 2025-02-10 Mon 14:45