Homework 4 Solutions

Solution Files

You can find the solutions in hw04.py.

Required Questions

Consult the drop-down if you need a refresher on the topics covered in this homework. It's okay to skip directly to the questions and refer back here should you get stuck.

Object-oriented programming (OOP) uses objects and classes to organize programs. Here's an example of a class:

class Car:
    max_tires = 4

    def __init__(self, color):
        self.tires = Car.max_tires
        self.color = color

    def drive(self):
        if self.tires < Car.max_tires:
            return self.color + ' car cannot drive!'
        return self.color + ' car goes vroom!'

    def pop_tire(self):
        if self.tires > 0:
            self.tires -= 1

Class: The type of an object. The Car class (shown above) describes the characteristics of all Car objects.

Object: A single instance of a class. In Python, a new object is created by calling a class.

>>> ferrari = Car('red')

Here, ferrari is a name bound to a Car object.

Class attribute: A variable that belongs to a class and is accessed via dot notation. The Car class has a max_tires attribute.

>>> Car.max_tires
4

Instance attribute: A variable that belongs to a particular object. Each Car object has a tires attribute and a color attribute. Like class attributes, instance attributes are accessed via dot notation.

>>> ferrari.color
'red'
>>> ferrari.tires
4
>>> ferrari.color = 'green'
>>> ferrari.color
'green'

Method: A function that belongs to an object and is called via dot notation. By convention, the first parameter of a method is self.

When one of an object's methods is called, the object is implicitly provided as the argument for self. For example, the drive method of the ferrari object is called with empty parentheses because self is implicitly bound to the ferrari object.

>>> ferrari = Car('red')
>>> ferrari.drive()
'red car goes vroom!'

We can also call the original Car.drive function. The original function does not belong to any particular Car object, so we must provide an explicit argument for self.

>>> ferrari = Car('red')
>>> Car.drive(ferrari)
'red car goes vroom!'

__init__: A special function that is called automatically when a new instance of a class is created.

Notice how the drive method takes in self as an argument, but it looks like we didn't pass one in! This is because the dot notation implicitly passes in ferrari as self for us. So in this example, self is bound to the object called ferrari in the global frame.

To evaluate the expression Car('red'), Python creates a new Car object. Then, Python calls the __init__ function of the Car class with self bound to the new object and color bound to 'red'.


Trees

Recall the tree abstract data type: a tree is defined as having a label and some branches. Previously, we implemented the tree abstraction using Python lists. Let's look at another implementation using objects instead:

class Tree:
    def __init__(self, label, branches=[]):
        for b in branches:
            assert isinstance(b, Tree)
        self.label = label
        self.branches = branches

    def is_leaf(self):
        return not self.branches

With this implementation, we can mutate a tree using attribute assignment, which wasn't possible in the previous implementation using lists. That's why we sometimes call these objects "mutable trees."

>>> t = Tree(3, [Tree(4), Tree(5)])
>>> t.label = 5
>>> t.label
5

To avoid redefining attributes and methods for similar classes, we can write a single base class from which more specialized classes inherit. For example, we can write a class called Pet and define Dog as a subclass of Pet:

class Pet:
    def __init__(self, name, owner):
        self.is_alive = True    # It's alive!!!
        self.name = name
        self.owner = owner
    def eat(self, thing):
        print(self.name + " ate a " + str(thing) + "!")
    def talk(self):
        print(self.name)

class Dog(Pet):
    def talk(self):
        super().talk()
        print('This Dog says woof!')

Inheritance represents a hierarchical relationship between two or more classes where one class is a more specific version of the other: a dog is a pet. (We use "is a" to describe this sort of relationship in OOP languages, not to refer to the Python is operator.)

Since Dog inherits from Pet, the Dog class will also inherit the Pet class's methods, so we don't have to redefine __init__ or eat. We do want each Dog to talk in a Dog-specific way, so we can override the talk method.

We can use super() to refer to the superclass of self, and access any superclass methods as if we were an instance of the superclass. For example, super().talk() in the Dog class will call the talk method from the Pet class, but passes in the Dog instance as the self.

Object-Oriented Programming

Q1: Vending Machine

In this question you'll create a vending machine that sells a single product and provides change when needed.

Implement the VendingMachine class, which models a vending machine for one specific product. The methods of a VendingMachine object return strings to describe the machine’s status and operations. Ensure that your output matches exactly with the strings provided in the doctests, including punctuation and spacing.

You may find Python's formatted string literals, or f-strings useful. A quick example:

>>> feeling = 'love'
>>> course = 'CS 61A!'
>>> combined_string = f'I {feeling} {course}'
>>> combined_string
'I love CS 61A!'
class VendingMachine:
    """A vending machine that vends some product for some price.

    >>> v = VendingMachine('candy', 10)
    >>> v.vend()
    'Nothing left to vend. Please restock.'
    >>> v.add_funds(15)
    'Nothing left to vend. Please restock. Here is your $15.'
    >>> v.restock(2)
    'Current candy stock: 2'
    >>> v.vend()
    'Please add $10 more funds.'
    >>> v.add_funds(7)
    'Current balance: $7'
    >>> v.vend()
    'Please add $3 more funds.'
    >>> v.add_funds(5)
    'Current balance: $12'
    >>> v.vend()
    'Here is your candy and $2 change.'
    >>> v.add_funds(10)
    'Current balance: $10'
    >>> v.vend()
    'Here is your candy.'
    >>> v.add_funds(15)
    'Nothing left to vend. Please restock. Here is your $15.'

    >>> w = VendingMachine('soda', 2)
    >>> w.restock(3)
    'Current soda stock: 3'
    >>> w.restock(3)
    'Current soda stock: 6'
    >>> w.add_funds(2)
    'Current balance: $2'
    >>> w.vend()
    'Here is your soda.'
    """
    def __init__(self, product: str, price: int):
        """Set the product and its price, as well as other instance attributes."""
self.product = product self.price = price self.stock = 0 self.balance = 0
def restock(self, n: int) -> str: """Add n to the stock and return a message about the updated stock level. E.g., Current candy stock: 3 """
self.stock += n return f'Current {self.product} stock: {self.stock}'
def add_funds(self, n: int) -> str: """If the machine is out of stock, return a message informing the user to restock (and return their n dollars). E.g., Nothing left to vend. Please restock. Here is your $4. Otherwise, add n to the balance and return a message about the updated balance. E.g., Current balance: $4 """
if self.stock == 0: return f'Nothing left to vend. Please restock. Here is your ${n}.' # Alternatively, we could have: # return self.vend() + f' Here is your ${n}.' self.balance += n return f'Current balance: ${self.balance}'
def vend(self) -> str: """Dispense the product if there is sufficient stock and funds and return a message. Update the stock and balance accordingly. E.g., Here is your candy and $2 change. If not, return a message suggesting how to correct the problem. E.g., Nothing left to vend. Please restock. Please add $3 more funds. """
if self.stock == 0: return 'Nothing left to vend. Please restock.' difference = self.price - self.balance if difference > 0: return f'Please add ${difference} more funds.' message = f'Here is your {self.product}' if difference != 0: message += f' and ${-difference} change' self.balance = 0 self.stock -= 1 return message + '.'

Use Ok to test your code:

python3 ok -q VendingMachine

Mutable Trees

Q2: Cumulative Mul

Write a function cumulative_mul that mutates the Tree t so that each node's label is replaced by the product of its label and the labels of all its descendents.

Hint: Be careful of the order in which you mutate the current node's label and process its subtrees; which one should come first?

def cumulative_mul(t: Tree) -> None:
    """Mutates t so that each node's label becomes the product of its own
    label and all labels in the corresponding subtree rooted at t.

    >>> t = Tree(1, [Tree(3, [Tree(5)]), Tree(7)])
    >>> cumulative_mul(t)
    >>> t
    Tree(105, [Tree(15, [Tree(5)]), Tree(7)])
    >>> otherTree = Tree(2, [Tree(1, [Tree(3), Tree(4), Tree(5)]), Tree(6, [Tree(7)])])
    >>> cumulative_mul(otherTree)
    >>> otherTree
    Tree(5040, [Tree(60, [Tree(3), Tree(4), Tree(5)]), Tree(42, [Tree(7)])])
    """
for b in t.branches: cumulative_mul(b) total = t.label for b in t.branches: total *= b.label t.label = total # Alternate solution using only one loop def cumulative_mul(t: Tree) -> None: for b in t.branches: cumulative_mul(b) t.label *= b.label

Use Ok to test your code:

python3 ok -q cumulative_mul

Q3: Prune Small

Removing some nodes from a tree is called pruning the tree.

Complete the function prune_small that takes in a Tree t and a number n. For each node with more than n branches, keep only the n branches with the smallest labels and remove (prune) the rest.

Hint: The max function takes in an iterable as well as an optional key argument (which takes in a one-argument function). For example, max([-7, 2, -1], key=abs) would return -7 since abs(-7) is greater than abs(2) and abs(-1).

The remove function takes in a value argument and mutates a list by removing the first occurrence of value. For example, if l = [1, 2, 3, 2], calling l.remove(2) would mutate l to be [1, 3, 2] because it removes the first 2 it encounters.

def prune_small(t: Tree, n: int) -> None:
    """Prune the tree mutatively, keeping only the n branches
    of each node with the smallest labels.

    >>> t1 = Tree(6)
    >>> prune_small(t1, 2)
    >>> t1
    Tree(6)
    >>> t2 = Tree(6, [Tree(3), Tree(4)])
    >>> prune_small(t2, 1)
    >>> t2
    Tree(6, [Tree(3)])
    >>> t3 = Tree(6, [Tree(1), Tree(3, [Tree(1), Tree(2), Tree(3)]), Tree(5, [Tree(3), Tree(4)])])
    >>> prune_small(t3, 2)
    >>> t3
    Tree(6, [Tree(1), Tree(3, [Tree(1), Tree(2)])])
    """
while len(t.branches) > n:
largest = max(t.branches, key=lambda x: x.label)
t.branches.remove(largest)
for b in t.branches:
prune_small(b, n)

Use Ok to test your code:

python3 ok -q prune_small

Inheritance

Q4: Cat

Below is the implementation of a Pet class. Each pet has two instance attributes (name and owner), as well as one instance method (talk).

class Pet:

    def __init__(self, name, owner):
        self.name = name
        self.owner = owner

    def talk(self):
        print(self.name)

Implement the Cat class, which inherits from the Pet class seen above.

class Pet:

    def __init__(self, name: str, owner: str) -> None:
        self.name = name
        self.owner = owner

    def talk(self) -> None:
        print(self.name)

class Cat(Pet):
    """
    >>> my_cat = Cat("Furball", "Me", lives=2)
    >>> my_cat.talk()
    Meow!
    >>> my_cat.name
    'Furball'
    >>> my_cat.lose_life()
    >>> my_cat.is_alive
    True
    >>> my_cat.eat("poison")
    Meow!
    Furball ate a poison!
    >>> my_cat.is_alive
    False
    >>> my_cat.lose_life()
    'Cat is dead x_x'
    """
    def __init__(self, name: str, owner: str, lives: int = 9) -> None:
        assert type(lives) == int and  lives > 0
Pet.__init__(self, name, owner) self.lives = lives self.is_alive = True
def talk(self) -> None: """A cat says 'Meow!' when asked to talk."""
print("Meow!")
def lose_life(self) -> str | None: """A cat can only lose a life if it has at least one life. When there are zero lives left, the 'is_alive' variable becomes False. """
if not self.is_alive: return "Cat is dead x_x" self.lives -= 1 if self.lives == 0: self.is_alive = False
def eat(self, thing: str) -> None: self.talk() print(f"{self.name} ate a {thing}!") if thing == "poison": self.is_alive = False

Use Ok to test your code:

python3 ok -q Cat

Check Your Score Locally

You can locally check your score on each question of this assignment by running

python3 ok --score

This does NOT submit the assignment! When you are satisfied with your score, submit the assignment to Gradescope to receive credit for it.

Submit Assignment

Submit this assignment by uploading any files you've edited to the appropriate Gradescope assignment. Lab 00 has detailed instructions.

Exam Practice

Homework assignments will also contain prior exam-level questions for you to take a look at. These questions have no submission component; feel free to attempt them if you'd like a challenge!

  1. Spring 2022 MT2 Q8: CS61A Presents The Game of Hoop.
  2. Spring 2024 Final Q5: Trees
  3. Spring 2026 MT Q5: Making Pen Friends!