This worksheet corresponds to material from lectures 4-5, which focus on these aspects of object-oriented programming:
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.
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
:
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")
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:
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")
Adding this method inside the definition of class Vector2
suffices:
def __bool__(self):
"considers vector false if both components are equal to zero and true otherwise"
if self.verbose:
print("called: {}.__bool__".format(self.__class__.__name__))
return self.x != 0 or self.y != 0
This is another problem about Point2
and Vector2
.
If instead of using these classes we stored coordinates in a list, e.g.
a = [5,3]
Then we couldn't use a.x
and a.y
to get the coordinates. Instead we'd access them by index:
a[0] # x coordinate is at index 0
a[1] # y coordinate is at index 1
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:
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[-3] # v.__getitem__(-3)
v["Irrational Exuberance"] # v.__getitem__("Irrational Exuberance")
Note the solution is identical for the point class and vector class. Just add this method to the class definition:
def __getitem__(self,i):
"obtain vector (or point) coordinates in list access format"
if i == 0:
return self.x
if i == 1:
return self.y
# If we get to this point, it's an invalid index
raise IndexError('Index must be 0 or 1')
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
delay
), a direction.delay
time units.class LazyMarchBot
class SpiralBot
steps
.Vector2(1,0)
, after steps
time units it makes a right hand 90-degree turn.Add these robots to the simulation and confirm they exhibit the expected behavior.
New additions to the bot class
class DelayMarchBot(Bot):
"Robot that waits for a given time then marches in a given direction"
symbol = "d"
def __init__(self, position, direction, delay_time):
"Constructor sets up bot with position, wait time and direction"
super().__init__(position)
#DelayMarchBot-specific initialization
self.delay_time = delay_time
self.direction = direction
def update(self):
"Stay put or take one step"
if self.delay_time > 0:
self.delay_time -= 1 #pause for one cycle
else:
self.move_by(self.direction) #use move from Bot class
class LazyBotMarch(WanderBot):
"Robot that randomly stays in places or walks in a given direction"
symbol = "L"
def __init__(self, position, direction):
"Sets up bot with position and direction"
super().__init__(position)
self.direction = direction
def update(self):
"Take one step or pause, randomly"
possible_steps = [
plane.Vector2(0, 0),
self.direction
]
step = random.choice(possible_steps) #chooses randomly a step in the given direction or a null step
self.move_by(step)
class SpiralBot(Bot):
"Robot that walks in a spiral of a given size"
symbol = "@"
spiral_directions = [
plane.Vector2(1, 0),
plane.Vector2(0, 1),
plane.Vector2(-1, 0),
plane.Vector2(0, -1),
]
def __init__(self, position, steps):
"Initialize a SpiralBot that walks in a spiral"
#Call the Bot constructor
super().__init__(position)
#SpiralBot specific initialization
self.steps = steps #tracks the length of the current line
self.current = steps #tracks how far along the current line bot is
self.dirtick = 0 #tracks what directions bot is moving
def update(self):
"Move robot in a spiral"
#note down direction in the terminal is increasing y coordinate
if self.steps > 1:
if self.current == 0:
self.dirtick += 1 #when bot reaches the end of the current line change direction
if self.dirtick % 2 == 0:
self.steps -= 1 #decrease the length of each line every two turns
self.current = self.steps
i = self.dirtick % 4 #direction rotates through possible_steps
self.move_by(self.spiral_directions[i])
self.current -= 1
Note that LazyMarchBot
could almost be made a subclass of WanderBot
, except that WanderBot
has its list of possible directions as a class attribute, while LazyMarchBot
must consider taking a step specified in its constructor (hence different for each instance).