实验 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 属性。该属性应是一个列表,用于记录账户发生的每笔交易。每次调用 deposit 或 withdraw 方法时,都应创建新的 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:电子邮件
电子邮件系统包含三个类: Email、 Server和 Client。 Client 可以
compose 一封邮件,并把它 send 给 Server。随后 Server 把邮件投递到另一个
inbox 的 Client。为了做到这一点, Server 具有名为
clients 的字典,把名称映射到相应的 Client 给 Client 实例。
假设 Client 永远不会更换所使用的 Server ,并且只能使用该 Email 来编写 Server.
请补全下面的定义以完成实现。 Email 类已经替你完成。
重要:开始前请完整阅读代码,理解各个类之间的关系,并注意方法的参数类型。思考每个方法中可以访问哪些变量,以及怎样借助它们访问其他类及其方法。
注意:
sender方法中的参数__init__(self, msg, sender, recipient_name)属于Client实例。client方法中的参数register_client(self, client)属于Server类,是一个Client实例。send(self, email)属于Server类是一个
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测试则会同时检查Server和Client类。因此,请先测试Server类的实现,再测试Client类。
使用 Ok 测试你的代码:
python3 ok -q Server
python3 ok -q Client
继承
两个类可能拥有相似属性,但其中一个是另一个的特殊情况。例如, Nickel 是 Coin的特殊情况。我们称 Nickel 是 Coin和 Coin 的子类,而后者是前者的基类(或超类) 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)。 - 一个
Coin的worth方法返回硬币的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