A document from MCS 275 Spring 2024, instructor Emily Dumas. You can also get the notebook file.

MCS 275 Spring 2024 Worksheet 3

  • Course instructor: Emily Dumas

Topics

This worksheet corresponds to material from lectures 4-5, which focus on these aspects of object-oriented programming:

  • Operator overloading
  • Subclasses and inheritance

Resources

These things might be helpful while working on the problems. Remember that for worksheets, we don't strictly limit what resources you can consult, so these are only suggestions.

0. Get ready to work on plane.py

First, download the plane.py module we wrote in lecture and save it somewhere you can find in the terminal and in VS code:

  • plane.py - download from the course sample code repo

The Point2 and Vector2 classes in this module provide lots of examples of operator overloading. In case you need a refesher on what they are all about, you can take a look at:

1. Boolean coercion

At the moment, if you use a Vector2 object in an if statement or any other place that a boolean is expected, it will always be considered True:

In [11]:
vzero = plane.Vector2(0,0)
vzerofloat = plane.Vector2(0.0,0.0)
v = plane.Vector2(1,1)
w = plane.Vector2(0,2)

if vzero:
    print("Vector of zeros (integer) is considered True")
if vzerofloat:
    print("Vector of zeros (float) is considered True")
if v:
    print(v,"is considered True")    
if w:
    print(w,"is considered True")
Vector of zeros (integer) is considered True
Vector of zeros (float) is considered True
Vector2(1,1) is considered True
Vector2(0,2) is considered True

That's not because of anything we put in the class definition, but because it's Python's default behavior. If a class doesn't specify how it should be coerced to a boolean, then it will always be considered True.

A more reasonable behavior for Vector2 is to consider it False if both components are equal to zero, and True otherwise. You can implement that behavior by adding a method __bool__ to Vector2 that takes no arguments and returns the boolean that represents the value of self.

Add this feature. If you are successful, the behavior of the same code shown above should change to:

In [13]:
vzero = plane.Vector2(0,0)          # now False
vzerofloat = plane.Vector2(0.0,0.0) # now False
v = plane.Vector2(1,1)              # now True
w = plane.Vector2(0,2)              # now True

if vzero:
    print("Vector of zeros (integer) is considered True")
if vzerofloat:
    print("Vector of zeros (float) is considered True")
if v:
    print(v,"is considered True")    
if w:
    print(w,"is considered True")
Vector2(1,1) is considered True
Vector2(0,2) is considered True

2. Indexed coordinate access

This is another problem about Point2 and Vector2.

If instead of using these classes we stored coordinates in a list, e.g.

In [14]:
a = [5,3]

Then we couldn't use a.x and a.y to get the coordinates. Instead we'd access them by index:

In [15]:
a[0] # x coordinate is at index 0
Out[15]:
5
In [16]:
a[1] # y coordinate is at index 1
Out[16]:
3

Sometimes it is nicer to have named attributes for coordinates, as in Point2 and Vector2, but the ability to refer to coordinates using index can also be convenient (e.g. if a loop needs to do the same thing to each coordinate).

To add indexing to Point2 and Vector2, add a special method __getitem__ to each class so that if a is an object of type Point2 or Vector2, then a[0] will return the x coordinate and a[1] will return the y coordinate. The way indexing relates to the special method is that a[i] evaluates to a.__getitem__(i).

Attempting to get any other index on a Point2 or Vector2 object should raise IndexError.

Here's a demo of the expected behavior:

In [18]:
v = plane.Vector2(5,7)
print("v.x =",v.x)
print("v[0] =",v[0])  # asking for v[0] calls v.__getitem__(0)
print("v.y =",v.y)
print("v[1] =",v[1])  # asking for v[1] calls v.__getitem__(1)
v.x = 5
v[0] = 5
v.y = 7
v[1] = 7
In [19]:
v[-3]  # v.__getitem__(-3)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
/tmp/ipykernel_4654/381668737.py in <module>
----> 1 v[-3]  # v.__getitem__(-3)

/tmp/ipykernel_4654/1846116892.py in g(self, k)
      2     if k in (0,1):
      3         return [self.x,self.y][k]
----> 4     raise IndexError("Index must be 0 or 1")
      5 plane.Vector2.__getitem__ = g

IndexError: Index must be 0 or 1
In [20]:
v["Irrational Exuberance"]   # v.__getitem__("Irrational Exuberance")
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
/tmp/ipykernel_4654/3241521498.py in <module>
----> 1 v["Irrational Exuberance"]   # v.__getitem__("Irrational Exuberance")

/tmp/ipykernel_4654/1846116892.py in g(self, k)
      2     if k in (0,1):
      3         return [self.x,self.y][k]
----> 4     raise IndexError("Index must be 0 or 1")
      5 plane.Vector2.__getitem__ = g

IndexError: Index must be 0 or 1

3. Additional bots

Download these files related to the robot simulation from the course sample code repository and put them in a directory where you'll do your work for this problem.

Note: I recommend you make a copy of the plane.py you edited in the previous problems (e.g. rename it plane2.py) and download a fresh copy of the original file. That way, if you accidentally introduced bugs into plane.py in your previous work, they won't affect this problem.

Now, open bots.py and add these new subclasses of Bot:

  • class DelayMarchBot

    • The constructor accepts a position, a number of time units (delay), a direction.
    • The robot that waits in its initial position for delay time units.
    • After the initial wait, the robot marches forever, taking steps in the given direction.
  • class LazyMarchBot

    • The constructor accepts a position and a direction.
    • At each time step, the robot chooses one of these two things to do based on a coin flip (random choice):
      • Take a step in a direction that was set in the constructor
      • Pause for a moment (do nothing)
  • class SpiralBot

    • In addition to its starting position, the constructor takes an argument steps.
    • Initially marching in the direction Vector2(1,0), after steps time units it makes a right hand 90-degree turn.
    • It continues marching and turning regularly so that it ultimately follows a spiral path like the one shown below.
    • When it gets to the end of the spiral, the robot stops moving but remains active.

spiral path

Add these robots to the simulation and confirm they exhibit the expected behavior.

Bonus round

Work on these open-ended problems if you finish the exercises above. We don't plan to include solutions to these in the worksheet solutions.

Vector2 and Point2 item assignment

Add a __setitem__ method to these classes so you can do things like a[0] = 15 to set the x coordinate (where a is a vector or point). Read about __setitem__ here (official Python docs).

MimicBot

Make a robot class that expects to be given an initial position and another bot (its "target") as constructor arguments. It should watch the other bot carefully, and on each time step it should make the same relative movement that the target bot did on the previous time step. That is, if at time n the target bot moved one unit right, then at time n+1 the MimicBot will move one unit right.

Revision history

  • 2024-01-21 Initial release