A document from MCS 260 Fall 2021, instructor David Dumas. You can also get the notebook file.

MCS 260 Fall 2021 Worksheet 10 Solutions

  • Course instructor: David Dumas
  • Solutions prepared by: Jennifer Vaccaro (2020 MCS 260 TA) and David Dumas

Topics

This worksheet focuses on object-oriented programming.

(While last week included some lecture material on higher-order functions and lamdba, they are not included on this worksheet because you'll work with those topics a lot on Project 3. But you can check out the higher/ folder in the course sample code repo to see several examples, including ones that were mentioned in lecture but not covered in detail.)

Problem 1 treated differently

Following the new policy, Problem 1 is different in that:

  • Tuesday lab students: Problem 1 will be presented and solved as a group, in a discussion led by your TA.
  • Thursday lab students: Please attempt problem 1 before coming to lab. Bring a solution, or bring questions. The problem will be discussed as a group.

Resources

The main course materials to refer to for this worksheet are:

(Lecture videos are not linked on worksheets, but are also useful to review while working on worksheets. Video links can be found in the course course Blackboard site.)

1. Segment class

Analogous to the classes discussed in our OOP lectures (for geometric objects Circle and Rectangle), create a Python class called Segment to store a line segment in the plane. The constructor should accept four arguments: x0,y0,x1,y1. The point (x0,y0) is then one endpoint of the segment, and (x1,y1) is the other endpoint. All four constructor arguments should be stored as attributes.

The following methods should be included:

  • translate(dx,dy) - move the segment horizontally by dx and vertically by dy. This should modify the object, not returning anything.
  • scale(factor) - increase the length of the segment by a factor of factor, keeping the center point the same. This is tricky, because the center point is not part of the data stored in the class! Feel free to skip this at first and come back to it later.
  • __str__() - make a reasonable human-readable string representation that includes the values of x0,y0,x1,y1.
  • length() - returns a float, the distance between the endpoints of the segment.

Also, read the next problem before you start work on this.

Solution

In [1]:
import math  # so that sqrt is available

class Segment():
    '''Creates a 2D line segment by storing the endpoints as attributes'''
    
    def __init__(self, x0, y0, x1, y1):
        '''Constructor stores the endpoint values to the Segment object'''
        self.x0 = x0
        self.y0 = y0
        self.x1 = x1
        self.y1 = y1
    
    def translate(self, dx, dy):
        '''Moves the Segment horizontally by dx, and vertically by dy. Returns nothing'''
        self.x0 += dx
        self.y0 += dy
        self.x1 += dx
        self.y1 += dy
    
    def scale(self, factor):
        '''Scales the Segment by `factor` while keeping the center point the same. Returns nothing'''
        # First, calculate the center of the segment using the averages of the endpoints
        centerx = (self.x0 + self.x1)/2
        centery = (self.y0 + self.y1)/2
        # Translate the segment so it is centered at the origin (0,0)
        self.translate(-centerx, -centery)
        # Scale the segment by multiplying the endpoint values by factor
        self.x0 *= factor
        self.x1 *= factor
        self.y0 *= factor
        self.y1 *= factor
        # Translate the scaled segment back to its original center
        self.translate(centerx, centery)
    
    def __str__(self):
        '''Human-readable string representing the Segment'''
        return "Segment(x0={},y0={},x1={},y1={})".format(self.x0, self.y0, self.x1, self.y1)
    
    def __repr__(self):
        '''Unambiguous string representation shown in REPL'''
        return self.__str__()
    
    def length(self):
        '''Returns the length of the segment calculated with the Pythagorean Thm'''
        dx = self.x1 - self.x0
        dy = self.y1 - self.y0
        return math.sqrt(dx**2 + dy**2)

2. Equality for segments

This problem builds on the Segment class from problem 1.

Suppose the Segment class from Problem 2 is stored in a file geom.py. Write a program to test the Segment class. It should do the following:

  • Create a segment and check that the attributes x0,y1, etc. exist.
  • Scale a segment and confirm that the new endpoints are as expected.
  • Translate a segment and confirm that the new endpoints are as expected.
  • Test that a segment that you choose to ensure its length is computed correctly.

IMPORTANT NOTE

Any time you are tempted to test whether two floats are equal, please instead use a check of whether they differ by a very small amount, e.g. instead of

if x == y:
    # stuff

use

if abs(x-y) < 0.000000001:
    # stuff

This will help to avoid problems created by the fact that float operations are only approximations of the associated operations on real numbers. For example,

0.1 + 0.2 == 0.3

evaluates to False because of the slight error in float addition (compared to real number addition), whereas

abs( 0.3 - (0.1+0.2) ) < 0.000000001

evaluates to True.

In [3]:
import geom

print("Create a Segment L from (0,0) to (0,4)")
L = geom.Segment(0, 0, 0, 4)
print("Confirm that L's endpoints are accessible")
print(L)
print(L.x0, L.y0, L.x1, L.y1)
print("Confirm that length is 4: ", abs(L.length()-4)<1e-12)
print("Try to scale L by 2 to (0,0),(0,8)")
L.scale(2)
print(L)
print("Confirm that length is 8: ", abs(L.length()-8)<1e-12)
print("Try to translate L to (50,100),(50,108)")
L.translate(50,100)
print(L)
Create a Segment L from (0,0) to (0,4)
Confirm that L's endpoints are accessible
Segment(x0=0,y0=0,x1=0,y1=4)
0 0 0 4
Confirm that length is 4:  True
Try to scale L by 2 to (0,0),(0,8)
Segment(x0=0.0,y0=-2.0,x1=0.0,y1=6.0)
Confirm that length is 8:  True
Try to translate L to (50,100),(50,108)
Segment(x0=50.0,y0=98.0,x1=50.0,y1=106.0)

3. Quantity with unit

Below you will find code that defines a class QuantityWithUnit that is meant to store float quantities that also have a unit of measurement attached to them, such as 55 m (55 meters), 2.8 s (2.8 seconds), or 94.05 kg (94.05 kilograms). Save it in a file qwu.py on your computer, so you can import it with import qwu. Read the code and familiarize yourself with what it does. Then try the following:

  • Create an instance M of this class to represent 19 kilograms
  • See how the REPL displays that value
  • Try printing M
  • What happens when you add M to itself?
  • What happens when you subtract M from itself?
  • Create an instance T of this class to represent 3600 seconds
  • What happens when you add M and T?

The final product of your work on this question could be a program that demonstrates all of these features.

In [6]:
class QuantityWithUnit:
    """A float quantity that also has a unit name, such as 
    kilograms, meters, seconds, etc."""
    def __init__(self,qty,unit):
        """Create new quantity with units"""
        self.qty = float(qty)
        self.unit = unit
    def __str__(self):
        """Make human-readable string version of quantity"""
        return "{} {}".format(self.qty,self.unit)
    def __repr__(self):
        """Make human-readable string version of quantity"""
        return "QuantityWithUnit(qty={},unit={})".format(self.qty,self.unit)
    def __add__(self,other):
        """Sum of two quantities requires them to have the same units"""
        if not isinstance(other,QuantityWithUnit):
            raise TypeError("Can only add a QuantityWithUnit to another QuantityWithUnit")
        if self.unit != other.unit:
            raise ValueError("Cannot add quantities with different units: {} and {}".format(self,other))
        return QuantityWithUnit(self.qty+other.qty,self.unit)
    def __sub__(self,other):
        """Difference of two quantities requires them to have the same units"""
        if not isinstance(other,QuantityWithUnit):
            raise TypeError("Can only subtract a QuantityWithUnit from another QuantityWithUnit")
        if self.unit != other.unit:
            raise ValueError("Cannot subtract quantities with different units: {} and {}".format(self,other))
        return QuantityWithUnit(self.qty-other.qty,self.unit)

Solution

In [3]:
import qwu

M = qwu.QuantityWithUnit(19, "kg")
print("M",M)
print("M.__str__()",M) # How print prints M
print("M.__repr__()",M.__repr__()) # How the REPL would show M
print("M+M",M+M)
print("M-M",M-M)
T = qwu.QuantityWithUnit(3600, "sec")
print("M+T",M+T)
M 19.0 kg
M.__str__() 19.0 kg
M.__repr__() QuantityWithUnit(qty=19.0,unit=kg)
M+M 38.0 kg
M-M 0.0 kg
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-3-6b2e17fcbbf0> in <module>
      7 print("M-M",M-M)
      8 T = qwu.QuantityWithUnit(3600, "sec")
----> 9 print("M+T",M+T)

~/Dropbox/teaching/mcs260/public/worksheets/qwu.py in __add__(self, other)
     17             raise TypeError("Can only add a QuantityWithUnit to another QuantityWithUnit")
     18         if self.unit != other.unit:
---> 19             raise ValueError("Cannot add quantities with different units: {} and {}".format(self,other))
     20         return QuantityWithUnit(self.qty+other.qty,self.unit)
     21     def __sub__(self,other):

ValueError: Cannot add quantities with different units: 19.0 kg and 3600.0 sec

4. Improving the quantity with unit class

You should look at the previous problem before attempting this one, because it presumes familiarity with the QuantityWithUnit class.

First, add support for testing equality to QuantityWithUnit. Two quantities with unit should be considered equal if the float quantities are equal, and if the units are equal.

Now, consider adding support for multiplication as follows.

Multiplying two quantities with units results in an answer that has a different unit. For example, 11 kilograms multiplied by 20 seconds is equal to 220 kilogram-seconds.

However, it does make sense to multiply a quantity with units by a number that has no units at all. For example, if you have 16 apples that each have a mass of 0.1 kilograms, then the total mass is (0.1kg)*16 = 1.6kg.

Add an operator overloading feature to QuantityWithUnit that allows such a quantity to be multiplied by a number as long as it is not an instance of QuantityWithUnit. If you do this correctly, then the following tests should behave as the comments suggest. These assume that you have QuantityWithUnit in a module called qwu.

Solution

In [ ]:
# content of qwu.py
# slightly modified from the class given in problem 3

class QuantityWithUnit:
    """A float quantity that also has a unit name, such as 
    kilograms, meters, seconds, etc."""
    def __init__(self,qty,unit):
        """Create new quantity with units"""
        self.qty = float(qty)
        self.unit = unit
    def __str__(self):
        """Make human-readable string version of quantity"""
        return "{} {}".format(self.qty,self.unit)
    def __repr__(self):
        """Make human-readable string version of quantity"""
        return "QuantityWithUnit(qty={},unit={})".format(self.qty,self.unit)
    def __add__(self,other):
        """Sum of two quantities requires them to have the same units"""
        if not isinstance(other,QuantityWithUnit):
            raise TypeError("Can only add a QuantityWithUnit to another QuantityWithUnit")
        if self.unit != other.unit:
            raise ValueError("Cannot add quantities with different units: {} and {}".format(self,other))
        return QuantityWithUnit(self.qty+other.qty,self.unit)
    def __sub__(self,other):
        """Difference of two quantities requires them to have the same units"""
        if not isinstance(other,QuantityWithUnit):
            raise TypeError("Can only subtract a QuantityWithUnit from another QuantityWithUnit")
        if self.unit != other.unit:
            raise ValueError("Cannot subtract quantities with different units: {} and {}".format(self,other))
        return QuantityWithUnit(self.qty-other.qty,self.unit)
    # ----------------------
    # NEW STUFF STARTS HERE
    # ----------------------    
    def __eq__(self, other):
        """Returns a True if the other is a QWU with the same qty and unit, False otherwise"""
        return self.__repr__() == other.__repr__()
    def __mul__(self, factor):
        """Returns a new QWU with the same units and the qty scaled by `factor` which must be a scalar."""
        try:
            # If `factor` is something we can convert to a float, we consider it
            # to be an acceptable thing to multiply by
            factor = float(factor)
        except TypeError:
            raise TypeError("Can only multiply a QuantityWithUnit by a scalar factor")
        return QuantityWithUnit(self.qty*factor, self.unit)
In [6]:
import qwu

car_mass = qwu.QuantityWithUnit(1200,"kg")
car_mass_2 = qwu.QuantityWithUnit(1200,"kg")
person_mass = qwu.QuantityWithUnit(68,"kg")
lecture_length = qwu.QuantityWithUnit(50,"min")

print("car_pass+person_mass:",car_mass + person_mass) # works, have same units
print("car_pass-person_mass:",car_mass - person_mass) # works, have same units
print("car_mass==person_mass:",car_mass == person_mass) # works, should print False
print("car_mass==car_mass_2:",car_mass == car_mass_2) # works, should print True
print("car_mass*5:",car_mass * 5) # works -- mass of 5 cars
print("car_mass*lecture_length:",car_mass * lecture_length) # fails (exception raised); only allowed to multiply by unitless numbers
car_pass+person_mass: 1268.0 kg
car_pass-person_mass: 1132.0 kg
car_mass==person_mass: False
car_mass==car_mass_2: True
car_mass*5: 6000.0 kg
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
~/Dropbox/teaching/mcs260/public/worksheets/qwu.py in __mul__(self, factor)
     35             # to be an acceptable thing to multiply by
---> 36             factor = float(factor)
     37         except TypeError:

TypeError: float() argument must be a string or a number, not 'QuantityWithUnit'

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
<ipython-input-6-08f00c3b709d> in <module>
     11 print("car_mass==car_mass_2:",car_mass == car_mass_2) # works, should print True
     12 print("car_mass*5:",car_mass * 5) # works -- mass of 5 cars
---> 13 print("car_mass*lecture_length:",car_mass * lecture_length) # fails (exception raised); only allowed to multiply by unitless numbers

~/Dropbox/teaching/mcs260/public/worksheets/qwu.py in __mul__(self, factor)
     36             factor = float(factor)
     37         except TypeError:
---> 38             raise TypeError("Can only multiply a QuantityWithUnit by a scalar factor")
     39         return QuantityWithUnit(self.qty*factor, self.unit)

TypeError: Can only multiply a QuantityWithUnit by a scalar factor

Revision history

  • 2021-10-28 Initial release