实验 6:面向对象编程

截止时间:7 月 16 日(星期四)晚上 11:59。

起始文件

下载 lab06.zip.

出勤要求

要获得实验课学分,除了到场参加实验课外,你还需要提交实验题目。

如果你因合理原因(例如生病或时间冲突)缺席实验课,或者由于某种原因未能完成签到,请在一周内发送邮件至 cs61a@berkeley.edu ,以补记出勤学分。

必做题

面向对象编程

下面复习面向对象编程。你也可以直接开始做题,遇到困难时再回来查阅。

面向对象编程 (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'.

问题 1:银行账户

扩展 BankAccount 类,为其添加 transactions 属性。该属性应是一个列表,用于记录账户发生的每笔交易。每次调用 depositwithdraw 方法时,都应创建新的 Transaction 实例并加入列表, 即使操作没有成功也要记录.

一个 Transaction 类实例应具有以下属性:

  • before:交易前的账户余额。
  • after:交易后的账户余额。
  • id:交易 ID,等于该账户此前发生的交易(存款或取款)数量。同一个 BankAccount 实例产生的交易 ID 必须唯一,但它 id 不需要在所有账户间全局唯一。换句话说,只需确保由同一个 Transaction 创建的两个 BankAccount 对象不会具有相同的 id.

此外, Transaction 类还应具有以下方法:

  • changed():如果余额发生变化(即 True ),则返回 before 不同于 after;否则返回 False.
  • report():返回描述交易的字符串。字符串应以交易 ID 开头,并说明余额变化。预期格式请参考 doctest。
class Transaction:
    def __init__(self, id: int, before: int, after: int):
        self.id = id
        self.before = before
        self.after = after

    def changed(self) -> bool:
        """Return whether the transaction resulted in a changed balance."""
        "*** YOUR CODE HERE ***"

    def report(self) -> str:
        """Return a string describing the transaction.

        >>> Transaction(3, 20, 10).report()
        '3: decreased 20->10'
        >>> Transaction(4, 20, 50).report()
        '4: increased 20->50'
        >>> Transaction(5, 50, 50).report()
        '5: no change'
        """
        msg: str = 'no change'
        if self.changed():
            "*** YOUR CODE HERE ***"
        return str(self.id) + ': ' + msg

class BankAccount:
    """A bank account that tracks its transaction history.

    >>> a = BankAccount('Eric')
    >>> a.deposit(100)    # Transaction 0 for a
    100
    >>> b = BankAccount('Erica')
    >>> a.withdraw(30)    # Transaction 1 for a
    70
    >>> a.deposit(10)     # Transaction 2 for a
    80
    >>> b.deposit(50)     # Transaction 0 for b
    50
    >>> b.withdraw(10)    # Transaction 1 for b
    40
    >>> a.withdraw(100)   # Transaction 3 for a
    'Insufficient funds'
    >>> len(a.transactions)
    4
    >>> len([t for t in a.transactions if t.changed()])
    3
    >>> for t in a.transactions:
    ...     print(t.report())
    0: increased 0->100
    1: decreased 100->70
    2: increased 70->80
    3: no change
    >>> b.withdraw(100)   # Transaction 2 for b
    'Insufficient funds'
    >>> b.withdraw(30)    # Transaction 3 for b
    10
    >>> for t in b.transactions:
    ...     print(t.report())
    0: increased 0->50
    1: decreased 50->40
    2: no change
    3: decreased 40->10
    """

    # *** YOU NEED TO MAKE CHANGES IN SEVERAL PLACES IN THIS CLASS ***

    def __init__(self, account_holder: str):
        self.balance: int = 0
        self.holder = account_holder

    def deposit(self, amount: int) -> int:
        """Increase the account balance by amount, add the deposit
        to the transaction history, and return the new balance.
        """
        self.balance = self.balance + amount
        return self.balance

    def withdraw(self, amount: int) -> int | str:
        """Decrease the account balance by amount, add the withdraw
        to the transaction history, and return the new balance.
        """
        if amount > self.balance:
            return 'Insufficient funds'
        self.balance = self.balance - amount
        return self.balance

使用 Ok 测试你的代码:

python3 ok -q BankAccount

问题 2:电子邮件

电子邮件系统包含三个类: EmailServerClientClient 可以 compose 一封邮件,并把它 sendServer。随后 Server 把邮件投递到另一个 inboxClient。为了做到这一点, Server 具有名为 clients 的字典,把名称映射到相应的 ClientClient 实例。

假设 Client 永远不会更换所使用的 Server ,并且只能使用该 Email 来编写 Server.

请补全下面的定义以完成实现。 Email 类已经替你完成。

重要:开始前请完整阅读代码,理解各个类之间的关系,并注意方法的参数类型。思考每个方法中可以访问哪些变量,以及怎样借助它们访问其他类及其方法。

注意:

  • sender 方法中的参数 __init__(self, msg, sender, recipient_name) 属于 Email 类,是一个 Client 实例。
  • client 方法中的参数 register_client(self, client) 属于 Server 类,是一个 Client 实例。
  • email 方法中的参数 send(self, email) 属于 Server 类是一个 Email 实例。
class Email:
    """An email has the following instance attributes:

        msg (str): the contents of the message
        sender (Client): the client that sent the email
        recipient_name (str): the name of the recipient (another client)
    """
    def __init__(self, msg: str, sender, recipient_name: str):
        self.msg = msg
        self.sender = sender
        self.recipient_name = recipient_name

class Server:
    """Each Server has one instance attribute called clients that is a
    dictionary from client names to client objects.

    >>> s = Server()
    >>> # Dummy client class implementation for testing only
    >>> class Client:
    ...     def __init__(self, server, name):
    ...         self.inbox = []
    ...         self.server = server
    ...         self.name = name
    >>> a = Client(s, 'Alice')
    >>> b = Client(s, 'Bob')
    >>> s.register_client(a) 
    >>> s.register_client(b)
    >>> len(s.clients)  # we have registered 2 clients
    2
    >>> all([type(c) == str for c in s.clients.keys()])  # The keys in self.clients should be strings
    True
    >>> all([type(c) == Client for c in s.clients.values()])  # The values in self.clients should be Client instances
    True
    >>> new_a = Client(s, 'Alice')  # a new client with the same name as an existing client
    >>> s.register_client(new_a)
    >>> len(s.clients)  # the key of a dictionary must be unique
    2
    >>> s.clients['Alice'] is new_a  # the value for key 'Alice' should now be updated to the new client new_a
    True
    >>> e = Email("I love 61A", b, 'Alice')
    >>> s.send(e)
    >>> len(new_a.inbox)  # one email has been sent to new Alice
    1
    >>> type(new_a.inbox[0]) == Email  # a Client's inbox is a list of Email instances
    True
    """
    def __init__(self):
        self.clients = {}

    def send(self, email: Email):
        """Append the email to the inbox of the client it is addressed to.
            email is an instance of the Email class.
        """
        ____.inbox.append(email)

    def register_client(self, client):
        """Add a client to the clients mapping (which is a 
        dictionary from client names to client instances).
            client is an instance of the Client class.
        """
        ____[____] = ____

class Client:
    """A client has a server, a name (str), and an inbox (list).

    >>> s = Server()
    >>> a = Client(s, 'Alice')
    >>> b = Client(s, 'Bob')
    >>> a.compose('Hello, World!', 'Bob')
    >>> b.inbox[0].msg
    'Hello, World!'
    >>> a.compose('CS 61A Rocks!', 'Bob')
    >>> len(b.inbox)
    2
    >>> b.inbox[1].msg
    'CS 61A Rocks!'
    >>> b.inbox[1].sender.name
    'Alice'
    """
    def __init__(self, server: Server, name: str):
        self.inbox: list = []
        self.server = server
        self.name = name
        server.register_client(____)

    def compose(self, message: str, recipient_name: str):
        """Send an email with the given message to the recipient."""
        email = Email(message, ____, ____)
        self.server.send(email)

本题有两个 ok 测试,必须全部通过才能获得满分。应按页面给出的顺序运行。 python3 ok -q Server 测试会检查 Server 类的实现,并不要求 Client 类已经正确实现。 python3 ok -q Client 测试则会同时检查 ServerClient 类。因此,请先测试 Server 类的实现,再测试 Client 类。

使用 Ok 测试你的代码:

python3 ok -q Server
python3 ok -q Client

继承

两个类可能拥有相似属性,但其中一个是另一个的特殊情况。例如, NickelCoin的特殊情况。我们称 NickelCoinCoin 的子类,而后者是前者的基类(或超类) Nickel。在 Python 中,下面这行代码 class Nickel(Coin): 建立这种关系。

class Coin:
    cents = None  # This will be provided by subclasses, but not by Coin itself

    def __init__(self, year):
        self.year = year

class Nickel(Coin):
    cents = 5

属于 Coin 的方法(例如 __init__)也会成为 Nickel的方法。因此,每个 Nickel 实例都具有 year 属性。不过,查找属性或方法时,会优先使用 Nickel 类中定义的版本。

>>> c = Nickel(1990)
>>> c.year
1990
>>> c.cents
5

问题 3:铸币厂

铸币厂是制造硬币的地方。本题将实现 Mint 类,使它能够产出具有正确年份和价值的 Coin

  • 每个 Mint 实例都具有 year 都有年份印记。 update 方法把实例的 year 年份印记设为 present_year 类的类属性 Mint .
  • create 方法接收 Coin 的一个子类(不是 实例),创建并返回该子类的实例,其年份印记为 Mint的年份(如果尚未更新,它可能不同于 Mint.present_year )。
  • 一个 Coinworth 方法返回硬币的 cents 基础价值;硬币年龄超过 50 年后,每多一年额外增加 1 美分。硬币年龄等于当前年份减去硬币年份。 present_year 类的类属性 Mint 类。
class Mint:
    """A mint creates coins by stamping on years.

    The update method sets the mint's stamp to Mint.present_year.

    >>> mint = Mint()
    >>> mint.year
    2025
    >>> dime = mint.create(Dime)
    >>> dime.year
    2025
    >>> Mint.present_year = 2105  # Time passes
    >>> nickel = mint.create(Nickel)
    >>> nickel.year     # The mint has not updated its stamp yet
    2025
    >>> nickel.worth()  # 5 cents + (80 - 50 years)
    35
    >>> mint.update()   # The mint's year is updated to 2105
    >>> Mint.present_year = 2180     # More time passes
    >>> mint.create(Dime).worth()    # 10 cents + (75 - 50 years)
    35
    >>> Mint().create(Dime).worth()  # A new mint has the current year
    10
    >>> dime.worth()     # 10 cents + (155 - 50 years)
    115
    >>> Dime.cents = 20  # Upgrade all dimes!
    >>> dime.worth()     # 20 cents + (155 - 50 years)
    125
    """
    present_year = 2025

    def __init__(self):
        self.update()

    def create(self, coin):
        "*** YOUR CODE HERE ***"

    def update(self) -> None:
        "*** YOUR CODE HERE ***"

class Coin:
    cents = None # will be provided by subclasses, but not by Coin itself

    def __init__(self, year: int):
        self.year = year

    def worth(self) -> int:
        "*** YOUR CODE HERE ***"

class Nickel(Coin):
    cents = 5

class Dime(Coin):
    cents = 10

使用 Ok 测试你的代码:

python3 ok -q Mint

在本地检查得分

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

python3 ok --score

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

提交作业

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

正确完成所有题目可得 1 分。 Please ensure your TA has taken your attendance before leaving.

可选题

以下题目为选做题。即使不完成,你仍能获得本次作业的学分;不过它们很适合用来练习,建议还是做一做!

问题 4:下一个 Virahanka Fibonacci 对象

请实现 next 对象的 VirFib 类。该类的 value 属性是一个 Fibonacci 数。 next 方法返回一个 VirFib 实例,其 value 是下一个 Fibonacci 数。 next 方法应只花费常数时间。

请注意,doctest 中没有打印任何内容;每次调用 .next() 都会返回一个 VirFib 实例。每个 VirFib 实例的显示方式由其 __repr__ 方法的返回值决定。

提示:可以在 next中设置新的实例属性来记录前一个数。对象可以在任何时候获得新实例属性,甚至可以在类定义之外设置。 __init__ 方法的返回值决定。

class VirFib():
    """A Virahanka Fibonacci number.

    >>> start = VirFib()
    >>> start
    VirFib object, value 0
    >>> start.next()
    VirFib object, value 1
    >>> start.next().next()
    VirFib object, value 1
    >>> start.next().next().next()
    VirFib object, value 2
    >>> start.next().next().next().next()
    VirFib object, value 3
    >>> start.next().next().next().next().next()
    VirFib object, value 5
    >>> start.next().next().next().next().next().next()
    VirFib object, value 8
    >>> start.next().next().next().next().next().next() # Ensure start isn't changed
    VirFib object, value 8
    """

    def __init__(self, value: int = 0):
        self.value = value

    def next(self):
        "*** YOUR CODE HERE ***"

    def __repr__(self) -> str:
        return "VirFib object, value " + str(self.value)

使用 Ok 测试你的代码:

python3 ok -q VirFib