CMU 15-112: Fundamentals of Programming and Computer Science
Homework 7 (Due Thursday 5-Mar at 8pm)


Note: this homework is due earlier than usual, on Thursday, because of spring break. It is also shorter than usual for the same reason.

Also note:
Required Problems

  1. Review Your Midterm1 Feedback [10 pts] [manually graded]
    Prior to this hw's deadline, you should open GradeScope and carefully review all the feedback from the graders on your midterm1 submission. If you have already done this, you do not have to do it again. Of course, we are promoting good habits. You should always "close the loop" and review graded materials, giving you one last chance to learn that material all the better. Also note that there is nothing to submit here, as GradeScope tracks when you read specific feedback items.

  2. shortAnswers() [10 pts] [autograded]
    In your hw7.py file, you will find the function shortAnswers(). Edit that function to answer the questions in the text it returns, carefully following these guidelines:
    • You should only edit the answer lines, which are the lines that start with the label A1, A2, ...
    • On each answer line, you should make a single edit, replacing each Z with one of A, B, C, or D for the multiple choice exercises, or with one of T or F for the true/false exercises.
    • Do not make any other edits! For example, do not add or remove any whitespace!

Mild + Medium Problems

  1. Bird, Penguin, and MessengerBird classes [40 pts] [autograded]
    In the hw7.py file, write the Bird, Penguin, and MessengerBird classes so that the following test code passes (and without hardcoding any cases, so any similar code would also pass). Also, you must use OOP properly (we may check this manually, after the autograder gives you a preliminary score).

    Note: the getLocalMethods() function is provided in the starter file. It is a helper function that our test functions use. We use it to be sure that you implement the correct methods in each class, and that you do not add extra unneeded methods in those classes. Also, note that getLocalMethods does not return static methods, so in particular stopMigrating will not be included there.

    def testBirdClasses():
        print("Testing Bird classes...", end="")
        # A basic Bird has a species name, can fly, and can lay eggs
        bird1 = Bird("Parrot")
        assert(type(bird1) == Bird)
        assert(isinstance(bird1, Bird))
        assert(bird1.fly() == "I can fly!")
        assert(bird1.countEggs() == 0)
        assert(str(bird1) == "Parrot has 0 eggs")
        bird1.layEgg()
        assert(bird1.countEggs() == 1)
        assert(str(bird1) == "Parrot has 1 egg")
        bird1.layEgg()
        assert(bird1.countEggs() == 2)
        assert(str(bird1) == "Parrot has 2 eggs")
        tempBird = Bird("Parrot")
        assert(bird1 == tempBird)
        tempBird = Bird("Wren")
        assert(bird1 != tempBird)
        assert(getLocalMethods(Bird) == ['__eq__','__init__', 
                                         '__repr__', 'countEggs', 
                                         'fly', 'layEgg'])
        
        # A Penguin is a Bird that cannot fly, but can swim
        bird2 = Penguin("Emperor Penguin")
        assert(type(bird2) == Penguin)
        assert(isinstance(bird2, Penguin))
        assert(isinstance(bird2, Bird))
        assert(not isinstance(bird1, Penguin))
        assert(bird2.fly() == "No flying for me.")
        assert(bird2.swim() == "I can swim!")
        bird2.layEgg()
        assert(bird2.countEggs() == 1)
        assert(str(bird2) == "Emperor Penguin has 1 egg")
        assert(getLocalMethods(Penguin) == ['fly', 'swim'])
        
        # A MessengerBird is a Bird that carries a message
        bird3 = MessengerBird("War Pigeon", "Top-Secret Message!")
        assert(type(bird3) == MessengerBird)
        assert(isinstance(bird3, MessengerBird))
        assert(isinstance(bird3, Bird))
        assert(not isinstance(bird3, Penguin))
        assert(not isinstance(bird2, MessengerBird))
        assert(not isinstance(bird1, MessengerBird))
        assert(bird3.deliverMessage() == "Top-Secret Message!")
        assert(str(bird3) == "War Pigeon has 0 eggs")
        assert(bird3.fly() == "I can fly!")
    
        bird4 = MessengerBird("Homing Pigeon", "")
        assert(bird4.deliverMessage() == "")
        bird4.layEgg()
        assert(bird4.countEggs() == 1)
        assert(getLocalMethods(MessengerBird) == ['__init__', 'deliverMessage'])
    
        # Note: all birds are migrating or not (together, as one)
        assert(bird1.isMigrating == bird2.isMigrating == bird3.isMigrating == False)
        assert(Bird.isMigrating == False)
    
        bird1.startMigrating()
        assert(bird1.isMigrating == bird2.isMigrating == bird3.isMigrating == True)
        assert(Bird.isMigrating == True)
    
        Bird.stopMigrating()
        assert(bird1.isMigrating == bird2.isMigrating == bird3.isMigrating == False)
        assert(Bird.isMigrating == False)
        print("Passed!")
    

  2. Marble, ConstantMarble, and DarkeningMarble classes [40 pts] [autograded]
    In the hw7.py file, write the Marble, ConstantMarble, and DarkeningMarble classes so that the following test code passes (and without hardcoding any cases, so any similar code would also pass). Also, you must use OOP properly (we may check this manually, after the autograder gives you a preliminary score).
    def testMarbleClasses():
        print("Testing Marble classes...", end="")
        # A Marble takes a string (not a list) of comma-separated color names
        m1 = Marble('Pink,Cyan')
        assert(m1.colorCount() == 2) # pink and cyan
        assert(Marble.getMarbleCount() == 1) # we have created 1 marble so far
    
        # When converted to a string, the Marble includes the color names,
        # each separated by a comma and a space, and all lower-case, and listed
        # in alphabetical order:
        assert(str(m1) == '<Marble with colors: cyan, pink>')
    
        m2 = Marble('Red,Orange,yellow,GREEN')
        assert(str(m2) == '<Marble with colors: green, orange, red, yellow>')
        assert(m2.colorCount() == 4)
        assert(Marble.getMarbleCount() == 2) # we have created 2 marbles so far
    
        # This also works in a list:
        assert(str([m1]) == '[<Marble with colors: cyan, pink>]')
    
        # Equality works as expected:
        m3 = Marble('red,blue')
        m4 = Marble('BLUE,RED')
        m5 = Marble('red,green,blue')
        assert((m3 == m4) and (m3 != m5) and (m3 != "Don't crash here!"))
        assert(Marble.getMarbleCount() == 5) # we have created 5 marbles so far
    
        # You can add colors, which only change the marble if they are not present:
        assert(m3.addColor('Red') == False) # False means the color was not added,
                                            # because it was already there
        # and no changes here:
        assert(m3.colorCount() == 2)
        assert(str(m3) == '<Marble with colors: blue, red>')
        assert((m3 == m4) and (m3 != m5))
    
        # Once more, but with a new color:
        assert(m3.addColor('green') == True) # True means the color was added!
        # and so these all change:
        assert(m3.colorCount() == 3)
        assert(str(m3) == '<Marble with colors: blue, green, red>')
        assert((m3 != m4) and (m3 == m5))
    
        # A ConstantMarble is a marble that never changes its color:
        m6 = ConstantMarble('red,blue')
        assert(isinstance(m6, Marble))
        assert(str(m6) == '<Marble with colors: blue, red>')
        assert(m6.addColor('green') == False) # constant marbles never change!
        assert(str(m6) == '<Marble with colors: blue, red>')
        assert(Marble.getMarbleCount() == 6) # we have created 6 marbles so far
        assert(getLocalMethods(ConstantMarble) == ['addColor'])
    
        # A DarkeningMarble is a marble that prefixes 'dark' to any colors
        # that are added after it is first created.
        # Note: for full credit, you must use super() properly here!
        m7 = DarkeningMarble('red,blue')
        assert(isinstance(m7, Marble))
        assert(str(m7) == '<Marble with colors: blue, red>') # not darkened
        assert(m7.addColor('green') == True) # but green will become darkgreen
        assert(str(m7) == '<Marble with colors: blue, darkgreen, red>')
        assert(Marble.getMarbleCount() == 7) # we have created 7 marbles so far
        assert(getLocalMethods(DarkeningMarble) == ['addColor'])
        print("Passed!")
    

Spicy Problem

  1. NamedNumber spicy class [85 pts] [autograded]
    In this exercise, we will use some special methods of Python to create a class that acts like an integer or float, but where we can specify the number using a string representation, as in 'forty two' instead of 42.

    Note: to complete this spicy exercise, you should first carefully watch the videos here. Be sure to watch the Class-Level Features part on static methods, too!

    Then, you may need to refer to some of the special method descriptions in the Python documentation here (see section 3.3, "Special method names"). The test functions list most of the special methods you may wish to read about (there are many others that are not required here). For example, testRichComparisonMethods() says that it tests __lt__, __le__, __gt__, and __ge__, so read about those!

    Also, while much of this exercise focuses on special methods, part of it focuses on cleverly converting to and from strings. When we grade, we may assign some extra spicy points for submissions with exceptionally short and clever functions that do these conversions.

    With that, write the class NamedNumber so that it passes all the provided test functions (see below). Be sure to use OOP properly!

    With that, here are the test cases for you to pass:
    def testNamedNumberBasics():
        print('  testNamedNumberBasics()...', end='')
    
        # Note that NamedNumber.nameToNumber must be a static method
        # you are responsible for:
        # integers and floats between (-1000, +1000) exclusive (so no thousands)
        # floats may have only one digit after the decimal point
        # Just follow the examples below (though do not hardcode them, of course)
        # Names will not contain dashes, nor the word 'and', etc...
        # You do not have to handle all the strange variants
        # like 'negative negative five'
        assert(NamedNumber.nameToNumber('zero') == 0)
        assert(NamedNumber.nameToNumber('forty') == 40)
        assert(NamedNumber.nameToNumber('fifty') == 50)
        assert(NamedNumber.nameToNumber('negative thirty two') == -32)
        assert(NamedNumber.nameToNumber('three hundred eighteen point eight') ==
                                        318.8)
        assert(NamedNumber.nameToNumber('negative nine hundred point five') ==
                                        -900.5)
        assert(NamedNumber.nameToNumber('four hundred eighty') == 480)
        assert(NamedNumber.nameToNumber('four hundred eighty two') == 482)
        assert(NamedNumber.nameToNumber('four hundred eighty two point one') ==
                                        482.1)
    
        # Similarly, NamedNumber.numberToName must be a static method
        # that does the same thing in reverse:
        assert(NamedNumber.numberToName(0) == 'zero')
        assert(NamedNumber.numberToName(40) == 'forty')
        assert(NamedNumber.numberToName(50) == 'fifty')
        assert(NamedNumber.numberToName(-32) == 'negative thirty two')
        assert(NamedNumber.numberToName(318.8) ==
               'three hundred eighteen point eight')
        assert(NamedNumber.numberToName(-900.5) ==
               'negative nine hundred point five')
        assert(NamedNumber.numberToName(480) == 'four hundred eighty')
        assert(NamedNumber.numberToName(482) == 'four hundred eighty two')
        assert(NamedNumber.numberToName(482.1) ==
               'four hundred eighty two point one')
    
        # Now let's test __init__, __repr__, and __eq__:
        x = NamedNumber('forty')
        assert(str(x) == repr(x) == 'NamedNumber(forty)')
        assert((x.name == 'forty') and (x.number == 40))
        assert((x == 40) and (x == 40.0))
        assert(x == NamedNumber('forty'))
        assert(x == 'forty') # this may be unexpected!
        assert(x != 50)
        assert(x != NamedNumber('fifty'))
        assert(x != 'do not crash here!')
    
        # We can create a named number using either the number or the name: 
        assert(NamedNumber(-42) == NamedNumber('negative forty two'))
        assert(NamedNumber(200.1)) == NamedNumber('two hundred point one')
    
        print('Passed!')
    
    def testNumericMethods():
        # Tests __float__, __add__, __radd__
        # (Naturally, similiar methods exist for -, *, /, //, **, %, ...)
        print('  testNumericMethods()...', end='')
        assert(float(NamedNumber('forty point three')) == 40.3)
        x = NamedNumber('forty')
        assert((x + 1) == NamedNumber('forty one')) # this uses __add__
        assert((2 + x) == NamedNumber('forty two')) # this uses __radd__
        assert((NamedNumber('point one') + x) == NamedNumber(40.1))
        assert(x == 40)
        x += 42
        assert(x == NamedNumber('eighty two'))
    
        # You should be able to figure out what other methods this requires
        # so that it can multiply to and subtract from NamedNumber instances...
        assert(NamedNumber(42)*10 - NamedNumber(20) == 400)
        print('Passed!')
    
    def testRichComparisonMethods():
        # Tests __lt__, __le__, __gt__, and __ge__
        print('  testRichComparisonMethods()...', end='')
        assert(NamedNumber(5.3) < NamedNumber(5.4))
        assert(NamedNumber(5.3) <= 5.4)
        assert(NamedNumber(5.4) > NamedNumber(5.3))
        assert(5.4 >= NamedNumber(5.3))
        print('Passed!')
    
    def testCallableObjects():
        # Tests __call__
        print('  testCallableObjects()...', end='')
        # This lets us *call* the object as though it were a function!
        # Here, the function will take a non-negative int n (don't worry about
        # other cases) and return a list containing n copies of this NamedNumber,
        # like so: 
        x = NamedNumber(42)
        assert(x(3) == [NamedNumber(42), NamedNumber(42), NamedNumber(42)])
        assert(x(0) == [])
        y = NamedNumber(-112)    
        assert(y(1) == ["negative one hundred twelve"])
        assert(y(3) == ["negative one hundred twelve", -112, NamedNumber(-112)])
        print('Passed!')
    
    def testNamedNumberClass():
        print('Testing NamedNumber Class...')
        testNamedNumberBasics()
        testNumericMethods()
        testRichComparisonMethods()
        testCallableObjects()
        print('Passed!')