家庭作业 4:面向对象编程、继承与可变树

截止时间:7 月 22 日(星期三)晚上 11:59

须知

下载 hw04.zip。压缩包内含 hw04.py文件以及 ok 自动评分器。

提交: 完成后,请将作业提交到 Gradescope。截止时间前可以多次提交,只有最后一次提交会被评分。请确认代码已成功提交;具体步骤参见 Lab 0 ,其中有更详细的作业提交说明。

使用 Ok: 如果对 Ok 的使用有任何疑问,请参阅 这份指南。

阅读材料: 以下参考资料可能对您有所帮助:

评分: 家庭作业按答案的正确性评分。每答错一道题,总分会扣 1 分。 本次家庭作业满分为 2 分。

必做题

如果需要复习本次作业涉及的知识,可以展开下面的内容。也可以直接开始做题,遇到困难时再回来查阅。

面向对象编程 (OOP)使用对象和类组织程序。下面是一个类的示例:
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

:对象的类型。上面展示的 Car 类描述了所有 Car 对象上调用。

对象:类的一个具体实例。在 Python 中,调用类即可创建新对象。

>>> ferrari = Car('red')

这里, ferrari 是绑定到一个 Car 对象的名称。

类属性:属于类的变量,通过点号访问。 Car 类具有 max_tires 属性。

>>> Car.max_tires
4

实例属性:属于某个具体对象的变量。每个 Car 对象都有 tires 属性和 color 属性。实例属性与类属性一样通过点号访问。

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

方法:属于对象并通过点号调用的函数。按照约定,方法的第一个参数是 self中的一个问题。

调用对象的方法时,该对象会被隐式地作为 self的实参传入。例如,调用 drive 对象的 ferrari 方法时只需空括号,因为 self 会隐式绑定到 ferrari 对象的名称。

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

也可以直接调用原始的 Car.drive 函数。原始函数不属于任何特定 Car 对象,因此必须显式提供 self中的一个问题。

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

__init__:创建类的新实例时自动调用的特殊函数。

请注意, drive 方法接收参数 self ,但看起来我们并没有传入它。这是因为点号会 隐式地ferrari 作为 self 传入。因此在这个例子中, self 绑定到全局帧中名为 ferrari 的对象。

为了对表达式 Car('red')求值,Python 会创建新的 Car 对象,然后调用 __init__ 类的 Car 函数,并把 self 绑定到新对象,把 color 绑定到 'red'中的一个问题。


回顾树抽象数据类型:一棵树由标签和若干分支组成。此前我们用 Python 列表实现了树抽象;下面看看如何改用对象实现。

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

采用这种实现后,可以通过给属性赋值来改变树;先前的列表实现无法这样做。因此,这些对象有时也称为“可变树”。

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

为了避免在相似的类中重复定义属性和方法,可以先编写一个 基类 ,让更具体的类从它 inherit。例如,可以编写名为 Pet 的类,再定义其 Dog as a 子类 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!')

继承表示两个或更多类之间的层次关系,其中一个类是另一个类的 is a 更具体版本:狗 is a 宠物。(在面向对象语言中,我们用“is a”描述这种关系,这里不是指 Python 的 is 运算符。)

Since Dog 继承自 Pet, the Dog 类也会继承 Pet 类的方法,因此无需重新定义 __init__eat。不过,我们希望每种 Dog 的根标签改为 talk in a Dog都有自己的特定行为,因此可以 重写 the talk 方法的返回值决定。

可以使用 super() 引用它的超类 self,并像超类实例一样访问超类方法。例如, super().talk() in the Dog 类会调用 talk 类中的方法 Pet ,但传入的是 Dog 实例作为 self中的一个问题。

面向对象编程

问题 1:自动售货机

本题将创建一台 自动售货机 ,它只销售一种商品,并在需要时找零。

实现 VendingMachine 类模拟销售某种特定商品的自动售货机。 VendingMachine 对象的方法返回描述机器状态和操作的字符串。请确保输出 exactly 与 doctest 中给出的字符串完全一致,包括标点和空格。

Python 的格式化字符串字面量(即 f-string )可能会很有用。下面是一个简单示例:

>>> 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."""
        "*** YOUR CODE HERE ***"

    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
        """
        "*** YOUR CODE HERE ***"

    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
        """
        "*** YOUR CODE HERE ***"

    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.
        """
        "*** YOUR CODE HERE ***"

使用 Ok 测试你的代码:

python3 ok -q VendingMachine

可变树

问题 2:累积乘积

编写函数 cumulative_mul ,它改变树 t ,把每个节点的标签替换为该标签与其所有后代节点标签的乘积。

提示:请留意修改当前节点标签与处理子树的先后顺序;哪一步应当先做?

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)])])
    """
    "*** YOUR CODE HERE ***"

使用 Ok 测试你的代码:

python3 ok -q cumulative_mul

问题 3:修剪较小分支

从树中移除部分节点称为 pruning 这棵树。

补全函数 prune_small ,它接收一棵树 Tree t 以及一个数 n。对于分支数超过 n 的每个节点,只保留标签最小的 n 个分支,并移除(prune)其余分支。

提示max 函数接收一个可迭代对象 iterable 以及可选的 key 参数(接收一个单参数函数)。例如, max([-7, 2, -1], key=abs) 会返回 -7 since abs(-7) 大于 abs(2)abs(-1)中的一个问题。

这个 remove 函数接收一个值 value ,改变列表并移除其中第一次出现的该值 value。例如,如果 l = [1, 2, 3, 2],调用 l.remove(2) 会把列表改变为 l to be [1, 3, 2] ,因为它移除了遇到的第一个 2

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 ____:
        largest = max(____, key=____)
        ____
    for b in t.branches:
        ____

使用 Ok 测试你的代码:

python3 ok -q prune_small

继承

Q4: Cat

下面给出了 Pet 类的实现。每只宠物有两个实例属性(nameowner),还有一个实例方法(talk).

class Pet:

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

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

实现 Cat 类,它继承自上面的 Pet 类。

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
        "*** YOUR CODE HERE ***"

    def talk(self) -> None:
        """A cat says 'Meow!' when asked to talk."""
        "*** YOUR CODE HERE ***"

    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.
        """
        "*** YOUR CODE HERE ***"

    def eat(self, thing: str) -> None:
        self.talk()
        print(f"{self.name} ate a {thing}!")
        if thing == "poison":
            self.is_alive = False

使用 Ok 测试你的代码:

python3 ok -q Cat

在本地检查得分

你可以运行以下命令,在本地检查本次作业每道题的得分:

python3 ok --score

这不会提交作业! 确认得分符合预期后,请将作业提交到 Gradescope,以获得作业学分。

提交作业

请上传所有你修改过的文件,将本次作业提交到 对应的 Gradescope 作业入口。 实验 00 中提供了详细说明。

考试练习

家庭作业还会附上往年考试难度的题目供你练习。这些题目无需提交;如果想挑战自己,可以自由尝试!

  1. 2022 春季期中考试 2 问题 8: CS61A 呈现:Hoop 游戏。
  2. 2024 春季期末考试问题 5:
  3. 2026 春季期中考试问题 5: 结交笔友!