This worksheet deviates from the original plan in order to account for the lost week of lecture time during the UICUF strike (Jan 17-22, 2023).
It includes some material on object-oriented programming, based on the discussion from the end of our Python tour (lecture 3) and the material on operator overloading in lecture 4.
Some of you will complete this worksheet after lecture 5, but that material will be covered on worksheet 4. In general, each worksheet after this one will focus on the previous week's lecture material. Usually that will mean 3 lectures of material is available for exploration on a worksheet.
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.
plane
module¶First, download the plane.py
module we wrote in lecture and save it somewhere you can find in the terminal and in VS code:
It has a couple of features we didn't discuss in lecture, such as scalar multiplication, e.g.
v = Vector2(1,5)
v*10 # test out Vector2.__mul__
and the reflected version, where the scalar comes before the vector but the vector object still handles the computation:
5*v # test out Vector2.__rmul__
I've also added unary plus and minus for vectors:
-v # negates all components
+v # same as v
Finally, there is now a method __abs__
that makes it so abs(v)
returns a float which is the length of the vector v
. It's natural to use abs
for this since in mathematics, both the length of a vector and the absolute value of a real number are referred to as the "magnitude" of the corresponding object.
abs(v)
This means you can find the distance between two points using abs(P-Q)
!
# Distance from (1,2) to (4,6) should be 5
abs(Point2(1,2) - Point2(4,6))
However, some things are missing, and this problem asks you to add them.
At the moment, vectors support addition but not subtraction. Fix that. The difference of two vectors u-v
should give a new vector with the property that (u-v)+v
is equal to u
.
v-v # subtraction doesn't work yet
The special method __bool__
decides whether an object evaluates to True
or False
if used in a place where a boolean is expected (e.g. if A:
is equivalent to if A.__bool__():
).
For numbers in Python, zero converts to False and all other numbers convert to True.
It would be natural to make Vector2
objects behave similarly, where the zero vector (with components 0,0
) evaluates to False
and all other vectors evaluate to True
.
Add a __bool__
method to the Vector2
class for this purpose. You can find more info about the __bool__
method at https://docs.python.org/3/reference/datamodel.html#object.__bool__
if v:
print("Not the zero vector")
else:
print("Zero Vector")
If v
is a vector or point, we can get the x component with v.x
. In some cases, it might be natural to also treat the vector or point like a list and retrieve the x component with v[0]
. Similarly, we'd want v[1]
to give the y component.
Thankfully, Python translates v[i]
into the method call v.__getitem__(i)
, so this is possible! Write a __getitem__
method for the Vector2
and Point2
classes so that index 0 gives the x component, index 1 gives the y component, and any other index raises the same type of error (IndexError
) you get when you ask for an invalid index from a list:
print(v[0])
print(v[1])
print(v[2])
class Point2:
"Point in the plane"
# Note: Base code (without additions) obtained from MCS 275 Spring 2023 Lecture 4 (plane.py)
def __init__(self, x, y):
"Initialize new point from x and y coordinates"
self.x = x
self.y = y
def __eq__(self, other):
"points are equal if and only if they have same coordinates"
if isinstance(other, Point2):
return (self.x == other.x) and (self.y == other.y)
else:
return False
def __add__(self, other):
"point+vector addition"
if isinstance(other, Vector2):
# we have been asked to add this Point2 to another Vector2
return Point2(self.x + other.x, self.y + other.y)
else:
# we have been asked to add this Point2 to some other object
# we don't want to allow this
return NotImplemented # return this to forbid the requested operation
def __sub__(self, other):
"point+vector subtraction"
if isinstance(other, Vector2):
# we have been asked to add this Point2 to another Vector2
return Point2(self.x - other.x, self.y - other.y)
else:
# we have been asked to add this Point2 to some other object
# we don't want to allow this
return NotImplemented # return this to forbid the requested operation
def __getitem__(self, index):
"obtain the x and y coordinates in list access format"
if index == 0:
return self.x
elif index == 1:
return self.y
else:
raise IndexError('list index out of range')
def __str__(self):
"human-readable string representation"
return "Point2({},{})".format(self.x, self.y)
def __repr__(self):
"unambiguous string representation"
return str(self)
def distance_to(self, other):
"get distance between two points"
if isinstance(other, Point2):
return abs(self - other)
else:
raise TypeError("distance_to requires argument of type Point2")
def __getitem__(self, index):
"obtain the x and y coordinates in list access format"
if index == 0:
return self.x
elif index == 1:
return self.y
else:
raise IndexError('list index out of range')
class Vector2:
"Displacement vector in the plane"
def __init__(self, x, y):
"Initialize new vector from x and y components"
self.x = x
self.y = y
def __eq__(self, other):
"vectors are equal if and only if they have same components"
if isinstance(other, Vector2):
return (self.x == other.x) and (self.y == other.y)
else:
return False
def __add__(self, other):
"vector addition"
if isinstance(other, Vector2):
# vector+vector = vector
return Vector2(self.x + other.x, self.y + other.y)
elif isinstance(other, Point2):
# vector + point = point
return Point2(self.x + other.x, self.y + other.y)
else:
# vector + anything else = nonsense
return NotImplemented # return this to forbid the requested operation
def __sub__(self, other):
"vector subtraction"
if isinstance(other, Vector2):
# vector-vector = vector
return Vector2(self.x - other.x, self.y - other.y)
else:
# vector - anything else = nonsense
return NotImplemented # return this to forbid the requested operation
def __mul__(self, other):
"vector-scalar multiplication"
if isinstance(other, (float, int)): # isinstance allows a tuple of types
# vector*scalar is vector
return Vector2(self.x * other, self.y * other)
else:
return NotImplemented
def __rmul__(self, other):
"scalar-vector multiplication"
# Called if other*self already attempted but failed
# for example if other is an int or float and self is a Vector2
# This "second chance" reflected version of multiplication lets the
# right hand operand decide what to do. In this case, we just decide
# that other*self is the same as self*other (handled by Vector2.__mul__ above)
return self * other
def __neg__(self):
"unary minus"
return Vector2(-self.x, -self.y)
def __pos__(self):
"unary plus: return a copy of the object"
return self
def __bool__(self):
"vector is not the zero vector"
return not(self.x == 0 and self.y == 0)
def __abs__(self):
"abs means length of a vector"
return (self.x * self.x + self.y * self.y) ** 0.5 # sqrt( deltax^2 + deltay^2 )
def __str__(self):
"human-readable string representation"
return "Vector2({},{})".format(self.x, self.y)
def __repr__(self):
"unambiguous string representation"
return str(self)
def __getitem__(self, index):
"obtain the vector x and y coordinates in list access format"
if index == 0:
return self.x
elif index == 1:
return self.y
else:
raise IndexError('list index out of range')
Antistr
¶In Physics, antimatter refers to a type of matter composed of particles that are "opposite" of the ones that make up the majority of the matter around us. For example, there are antiprotons, antielectrons (positrons), etc..
When a particle of matter collides with its corresponding antiparticle, the two particles annihilate and a huge amount of energy is released. (For this reason, keeping any amount of antimatter around is both dangerous and difficult.)
Make a class Antistr
that behaves like an "antimatter" to ordinary Python strings: An Antistr
can be created from a string, but then represents the sequence of "anticharacters" of all the characters in the string.
Adding strings is like putting matter together. Usually, when you add two Python strings you just get a longer string obtained by joining the two strings together:
"van" + "illa"
But if you add a Python string to the corresponding antistring (Antistr
), they should annihilate and release energy. This should correspond to the message "BOOM!" being printed on the terminal, and an empty string being returned:
Antistr("quail") + "quail" # prints a message about energy release, then returns empty str
Antistr("quail") + "quail" # prints a message about energy release, then returns empty str
"shark" + Antistr("shark")
More generally, it should be possible for an antistring to annihilate just part of a string, as long as the string ends with or begins with the same characters as are in the antistring:
"carpet" + Antistr("pet") # anti-pet annihilates pet, leaving just car
Antistr("un") + "uninspired"
However, if any anticharacters are left over in this process (don't annihilate identical characters from the string), then an exception should be raised.
# time and anti-time annihilate, but antistring " for a snack" is left
# and that is an error
"extensive downtime" + Antistr("time for a snack")
To do this, you'll need to have suitable __add__
and __radd__
methods in your Antistr
class, as well as a constructor that accepts a string.
class Antistr:
"Antistr class models behavior of an antistring"
def __init__(self,s):
"Initialize a new antistring with a string attribute"
self.s = s
def __add__(self,other):
"antistring addition/annihilation operation"
if isinstance(other,str):
if other.startswith(self.s):
print("BOOM!")
return other[len(self.s):]
else:
raise ValueError("Operation results in dangerous unshielded antistring")
def __radd__(self,other):
"antistring right operand addition/annihilation operation"
if isinstance(other,str):
if other.endswith(self.s):
print("BOOM!")
return other[:-len(self.s)]
else:
raise ValueError("Operation results in dangerous unshielded antistring")
def __repr__(self):
"unambiguous string representation"
return str(self)
This exercise is about making a class that can change state depending on a specified pattern of responses to external input. It's not about operator overloading.
Imagine a simplified thermostat that controls heating and cooling of a hotel room.
It has three buttons: Up, Down, and Mode. It keeps track of the user's desired temperature, the room temperature, and it can turn two devices on or off: a heater and an air conditioner.
Pressing "up"
increases the desired temperature by one degree, unless the system is in "off" mode in which case it does nothing.
Pressing "down"
increases the desired temperature by one degree, unless the system is in "off" mode in which case it does nothing.
Pressing "mode"
button cycles between operating modes in this order: heat -> cool -> auto -> off (after which it repeats the cycle, going back to Heat).
When the system is in Heat, Cool, or Auto mode, it shows the desired temperature on its display panel. But in Off mode, it knows the last-set desired temperature, but does not show it anywhere.
The mode, desired temp, and room temp determine what the thermostat does as follows:
Write a class Thermostat
with the following attributes and methods that can be used to simulate this system:
ac_is_on
- attribute, a boolean, always indicating whether the AC is turned onheat_is_on
- attribute, a boolean, always indicating whether the heater is turned on__init__(self)
- method, initializes a new thermostat in which the mode is "off"
, and both the room and the desired temperature are 68
.room_temp(self,x)
- method, tells the thermostat that the room temperature is x
and have it react accordingly.press(self,btn)
- method, simulates the press of a button; the value of btn
should be one of "up"
, "down"
, or "mode"
.get_display(self)
- method, retrieves the text currently displayed on the control panel, in one of these formats:"72/cool"
in cool mode (with 72 being the desired temp)"65/heat"
in heat mode (with 65 being the desired temp)"70/auto"
in auto mode (with 70 being the desired temp)"--/off"
in off mode__str__(self)
and __repr__(self)
- methods that return the same thing, a string in this format:
Thermostat(mode="off",display="--/off",room=68,desired=68,ac_is_on=False,heat_is_on=False)
Hints:
Below is an example of using the class, with commentary.
T = Thermostat()
print(T) # Shows initial state
T.press("up") # In "off" mode, pressing up does nothing. Still set to 68
print(T)
T.press("mode") # Cycle from "off" to "heat" mode
# Room still at desired temp, so heater and AC are both off
print(T)
T.room_temp(67) # Temp now too low, so the heater will turn on
print(T)
T.press("down") # Desired temp goes down, now equal to room temp, so heater off
print(T)
T.press("mode") # Cycle from "heat" to "cool" mode. Heater and AC off.
print(T)
T.press("mode") # Cycle from "cool" to "auto" mode. Heater and AC off.
print(T)
T.room_temp(72) # Room temp is now too high, AC will turn on.
print(T)
T.room_temp(59) # Room temp is now too low, heater will turn on.
print(T)
# Repeatedly lower the desired temp while in auto mode
# For a while, heat will stay on.
# Then the room temp and desired temp will be equal, and both heat and AC will be off
# Then the room temp will be higher than the desired temp, and the AC will turn on
for i in range(10):
T.press("down")
print(T)
class Thermostat:
"Thermostat module class models thermostat status and state transitions"
def __init__(self):
"Initialize a new thermostat object with default settings"
self.next_mode = {
"off": "heat",
"heat": "cool",
"cool": "auto",
"auto": "off",
}
self.mode = "off"
self.room = 68
self.desired = 68
self.ac_is_on = False
self.heat_is_on = False
def room_temp(self,x):
"set the room temperature and update state status"
self.room = x
self.update()
def ac_is_on(self):
"is the ac on?"
return self.ac_is_on
def heat_is_on(self):
"is the heat on?"
return self.heat_is_on
def get_display(self):
"retrieves the control panel text"
if self.mode == "off":
numstr = "--"
else:
numstr = str(self.desired)
return numstr + "/" + self.mode
def press(self,btn):
"pressing a button on panel and adjusting temperature settings"
if btn == "mode":
self.mode = self.next_mode[self.mode]
else:
if self.mode == "off":
return
elif btn == "up":
self.desired += 1
elif btn == "down":
self.desired -= 1
self.update()
def update(self):
"adjusts heat/ac control based on desired temp and room temp"
self.heat_is_on = (self.mode in ["heat","auto"]) and (self.room < self.desired)
self.ac_is_on = (self.mode in ["ac","auto"]) and (self.room > self.desired)
def __str__(self):
"readable display of thermostat status data"
return "Thermostat(mode=\"{}\",display=\"{}\",room={},desired={},ac_is_on={},heat_is_on={})".format(
self.mode,
self.get_display(),
self.room,
self.desired,
self.ac_is_on,
self.heat_is_on)
def __repr__(self):
"readable display of thermostat status data"
return str(self)