← Back to course
Python 11-12 / Week 03 / Wednesday
3/6
Week 03 · Guarding the State

Wednesday

Sim Lab: rover.py
// Design a class first, then hide its data behind methods
⏱ about 30 min

Wednesday: Sim Lab: rover.py

Comet wants Pip to turn. "Left and right," she says. "N, E, S, W, round and round. That is just a list!"

Wren writes the four headings in a circle on his graph paper. "What does the evidence say happens after W?" he asks. "The list ends, but the circle keeps going."

Nova projects the list with an arrow that jumps from the end back to the start. "How can I help?" she asks. "Try the remainder operator, %, in the shell first."

Comet hands you the keyboard. "Lead programmer, stage one. Then we turn."

Sim Lab: the real rover.py

rover.py is the second real FieldSim file. drive_test.py is a short driver that imports Rover and tries it out. Save both in your fieldsim folder.

  1. Stage 1: type the state, the getters and status() in rover.py.
  2. Stage 2: add the two turn methods and the class variable.
  3. Stage 3: run drive_test.py and check four right turns come back to E.
  4. Stage 4: break turn_right() on purpose, read the error and fix it.

Stage 1: state and getters

# rover.py
# A Rover drives on the test field, one cell at a time.

HEADINGS = ["N", "E", "S", "W"]


class Rover:
    """A survey rover on the Larkspur test field."""

    def __init__(self, name, row=0, col=0):
        self.name = name
        self._row = row
        self._col = col
        self._heading = "E"
        self._battery = 100

    def get_row(self):
        return self._row

    def get_col(self):
        return self._col

    def get_heading(self):
        return self._heading

    def get_battery(self):
        return self._battery

    def status(self):
        return (f"{self.name} at ({self._row}, {self._col}) facing "
                f"{self._heading}, battery {self._battery}")
# drive_test.py
from rover import Rover

pip = Rover("Pip")
print(pip.status())
print(pip.get_row(), pip.get_col(), pip.get_heading())

Stage 2: turning with HEADINGS and %

>>> HEADINGS = ["N", "E", "S", "W"]
>>> HEADINGS.index("E")
1
>>> HEADINGS[(1 + 1) % 4]
'S'
>>> HEADINGS.index("W")
3
>>> (3 + 1) % 4
0
>>> (0 - 1) % 4
3

index() gives the zero-based position of the first matching item. The shell shows % 4 turns 4 back into 0, and -1 into 3, so the list wraps like a circle.

turn_left() and turn_right() are mutators: they change _heading and give nothing back. HEADINGS sits at the top of rover.py from stage 1 on.

    def turn_left(self):
        i = HEADINGS.index(self._heading)
        self._heading = HEADINGS[(i - 1) % 4]

    def turn_right(self):
        i = HEADINGS.index(self._heading)
        self._heading = HEADINGS[(i + 1) % 4]
TRACE THE TURNS
  • Read the question.
  • Tap your answer.
Pip faces N and turns left. What is (i - 1) % 4?
So which way does Pip face after that left turn?

Stage 3: the whole rover.py, and a drive test

# rover.py
# A Rover drives on the test field, one cell at a time.

HEADINGS = ["N", "E", "S", "W"]


class Rover:
    """A survey rover on the Larkspur test field."""

    rovers_built = 0

    def __init__(self, name, row=0, col=0):
        self.name = name
        self._row = row
        self._col = col
        self._heading = "E"
        self._battery = 100
        Rover.rovers_built += 1

    def get_row(self):
        return self._row

    def get_col(self):
        return self._col

    def get_heading(self):
        return self._heading

    def get_battery(self):
        return self._battery

    def turn_left(self):
        i = HEADINGS.index(self._heading)
        self._heading = HEADINGS[(i - 1) % 4]

    def turn_right(self):
        i = HEADINGS.index(self._heading)
        self._heading = HEADINGS[(i + 1) % 4]

    def status(self):
        return (f"{self.name} at ({self._row}, {self._col}) facing "
                f"{self._heading}, battery {self._battery}")
# drive_test.py
from rover import Rover

pip = Rover("Pip")
print(pip.status())
pip.turn_right()
print(pip.get_heading())
pip.turn_right()
pip.turn_right()
pip.turn_right()
print(pip.get_heading())
pip.turn_left()
print(pip.status())
print(Rover.rovers_built)
PREDICT DRIVE_TEST.PY
  • Read the question.
  • Tap your answer.
After four right turns, which way does Pip face?
What is the fourth line?
Pip at (0, 0) facing E, battery 100
S
E
Pip at (0, 0) facing N, battery 100
1

Stage 4: drop the % 4

In rover.py, change the last line of turn_right() to self._heading = HEADINGS[i + 1]. Save it and run drive_test.py.

# drive_test.py
from rover import Rover

pip = Rover("Pip")
print(pip.status())
pip.turn_right()
print(pip.get_heading())
pip.turn_right()
pip.turn_right()
pip.turn_right()
print(pip.get_heading())
pip.turn_left()
print(pip.status())
print(Rover.rovers_built)
Pip at (0, 0) facing E, battery 100
S
Traceback (most recent call last):
  File "drive_test.py", line 9, in <module>
    pip.turn_right()
    ~~~~~~~~~~~~~~^^
  File "rover.py", line 38, in turn_right
    self._heading = HEADINGS[i + 1]
                    ~~~~~~~~^^^^^^^
IndexError: list index out of range
READ THE INDEXERROR
  • Read the question.
  • Tap your answer.
Which way did Pip face when the error happened?
What is the fix?
At your computer
1. Stage 1: type rover.py up to status() and save it in fieldsim. Type drive_test.py from stage 1 and run it.
2. Stage 2: add the turn methods, rovers_built = 0 and Rover.rovers_built += 1, as in the full rover.py.
3. Stage 3: change drive_test.py to the stage 3 version. Predict each line, then run it.
4. Stage 4: drop the % 4, run drive_test.py and read the error. Put % 4 back and run it again.

rover.py now turns and guards its state. Tomorrow: the one variable that every rover shares.

← Tuesday