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

MCS 275 Spring 2022 Worksheet 3

  • Course instructor: David Dumas

Topics

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.

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.

1. Adding features to the 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:

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

It has a couple of features we didn't discuss in lecture, such as scalar multiplication, e.g.

In [3]:
v = plane.Vector2(1,5)
v*10  # test out Vector2.__mul__
Out[3]:
Vector2(10,50)

and the reflected version, where the scalar comes before the vector but the vector object still handles the computation:

In [4]:
5*v  # test out Vector2.__rmul__
Out[4]:
Vector2(5,25)

I've also added unary plus and minus for vectors:

In [5]:
-v  # negates all components
Out[5]:
Vector2(-1,-5)
In [6]:
+v  # same as v
Out[6]:
Vector2(1,5)

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.

In [9]:
abs(v)
Out[9]:
5.0990195135927845

This means you can find the distance between two points using abs(P-Q)!

In [11]:
# Distance from (1,2) to (4,6) should be 5
abs( plane.Point2(1,2) - plane.Point2(4,6) )
Out[11]:
5.0

However, some things are missing, and this problem asks you to add them.

A. Vector subtraction

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.

In [8]:
v-v  # subtraction doesn't work yet
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_198085/1335678657.py in <module>
----> 1 v-v  # subtraction doesn't work yet

TypeError: unsupported operand type(s) for -: 'Vector2' and 'Vector2'

B. Boolean coercion

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__

C. Indexed item access

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:

2. 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 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:

In [15]:
"van" + "illa"
Out[15]:
'vanilla'

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:

In [31]:
Antistr("quail") + "quail"  # prints a message about energy release, then returns empty str
BOOM!
Out[31]:
''
In [25]:
"shark" + Antistr("shark")
BOOM!
Out[25]:
''

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:

In [27]:
"carpet" + Antistr("pet")  # anti-pet annihilates pet, leaving just car
BOOM!
Out[27]:
'car'
In [28]:
Antistr("un") + "uninspired"
BOOM!
Out[28]:
'inspired'

However, if any anticharacters are left over in this process (don't annihilate identical characters from the string), then an exception should be raised.

In [30]:
# time and anti-time annihilate, but antistring " for a snack" is left 
# and that is an error
"extensive downtime" + Antistr("time for a snack") 
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/tmp/ipykernel_198085/633364138.py in <module>
      1 # time and anti-time annihilate, but antistring " for a snack" is left
      2 # and that is an error
----> 3 "extensive downtime" + Antistr("time for a snack")

/tmp/ipykernel_198085/2992101141.py in __radd__(self, other)
     15                 return other[:-len(self.s)]
     16             else:
---> 17                 raise ValueError("Operation results in dangerous unshielded antistring")

ValueError: Operation results in dangerous unshielded antistring

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.

3. Thermostat model

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. The physical interface might look like this:

It has three buttons: "up", "down", and "mode". It keeps track of the user's desired temperature, the room temperature (which isn't shown), 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 most recently set desired temperature, but does not show it on the display.

The mode, desired temp, and room temp determine what the thermostat does as follows:

  • If the system is in Heat or Auto mode, the heater is turned on exactly when the room temperature is lower than the desired temperature; in any other mode, the heater is off.
  • If the system is in Cool or Auto mode, the AC is turned on exactly when the room temperature is higher than the desired temperature; in any other mode, the AC if off.

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 on
  • heat_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:

  • Have attributes of the class that represent its current state (i.e. mode, room temp, desired temp)
  • The response to button presses will behave differently depending on aspects of that state, and may modify the state attributes

Below is an example of using the class, with commentary.

In [164]:
T = Thermostat()
print(T) # Shows initial state
Thermostat(mode="off",display="--/off",room=68,desired=68,ac_is_on=False,heat_is_on=False)
In [165]:
T.press("up") # In "off" mode, pressing up does nothing.  Still set to 68
print(T)
Thermostat(mode="off",display="--/off",room=68,desired=68,ac_is_on=False,heat_is_on=False)
In [166]:
T.press("mode") # Cycle from "off" to "heat" mode
# Room still at desired temp, so heater and AC are both off
print(T)
Thermostat(mode="heat",display="68/heat",room=68,desired=68,ac_is_on=False,heat_is_on=False)
In [167]:
print(T.get_display())  # Show just what would be on the display
68/heat
In [168]:
T.room_temp(67) # Temp now too low, so the heater will turn on
print(T)
Thermostat(mode="heat",display="68/heat",room=67,desired=68,ac_is_on=False,heat_is_on=True)
In [169]:
T.press("down") # Desired temp goes down, now equal to room temp, so heater off
print(T)
Thermostat(mode="heat",display="67/heat",room=67,desired=67,ac_is_on=False,heat_is_on=False)
In [170]:
T.press("mode") # Cycle from "heat" to "cool" mode.  Heater and AC off.
print(T)
Thermostat(mode="cool",display="67/cool",room=67,desired=67,ac_is_on=False,heat_is_on=False)
In [171]:
T.press("mode") # Cycle from "cool" to "auto" mode.  Heater and AC off.
print(T)
Thermostat(mode="auto",display="67/auto",room=67,desired=67,ac_is_on=False,heat_is_on=False)
In [172]:
T.room_temp(72) # Room temp is now too high, AC will turn on.
print(T)
Thermostat(mode="auto",display="67/auto",room=72,desired=67,ac_is_on=True,heat_is_on=False)
In [173]:
T.room_temp(59) # Room temp is now too low, heater will turn on.
print(T)
Thermostat(mode="auto",display="67/auto",room=59,desired=67,ac_is_on=False,heat_is_on=True)
In [174]:
# 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)
Thermostat(mode="auto",display="66/auto",room=59,desired=66,ac_is_on=False,heat_is_on=True)
Thermostat(mode="auto",display="65/auto",room=59,desired=65,ac_is_on=False,heat_is_on=True)
Thermostat(mode="auto",display="64/auto",room=59,desired=64,ac_is_on=False,heat_is_on=True)
Thermostat(mode="auto",display="63/auto",room=59,desired=63,ac_is_on=False,heat_is_on=True)
Thermostat(mode="auto",display="62/auto",room=59,desired=62,ac_is_on=False,heat_is_on=True)
Thermostat(mode="auto",display="61/auto",room=59,desired=61,ac_is_on=False,heat_is_on=True)
Thermostat(mode="auto",display="60/auto",room=59,desired=60,ac_is_on=False,heat_is_on=True)
Thermostat(mode="auto",display="59/auto",room=59,desired=59,ac_is_on=False,heat_is_on=False)
Thermostat(mode="auto",display="58/auto",room=59,desired=58,ac_is_on=True,heat_is_on=False)
Thermostat(mode="auto",display="57/auto",room=59,desired=57,ac_is_on=True,heat_is_on=False)