Skip to main content

讨论 7 | CS 61A 2024 年春季

讨论 7:面向对象编程

小组里派个人加入 [Discord] 吧 (https://cs61a.org/articles/discord)。多个人加入也行,但一个人就够了。

现在切换到 Pensieve:

  • 所有人:前往 [discuss.pensieve.co],用 @berkeley.edu 邮箱登录,然后输入组号 (就是你们的 Discord 频道号)。

进了 Pensieve 之后,就不用再回到这个页面了;Pensieve 上有同样的内容 (而且功能更多)。如果 Pensieve 出了什么问题,再回来这个页面继续讨论。

如果您遇到问题,请在 Discord 上的 #help 频道中发帖。

开始

先说说自己的名字,除了 CS 61A 还在上什么课,还有一直在练习的东西,比如乐器、杂耍、武术之类的。看看大家有没有什么共同爱好?

Q1:抽牌

draw 函数接受一个列表 hand 和一个唯一的非负整数列表 positions,它们都小于 hand 的长度。对于 positions 里的每个 p,这个函数会移除 hand[p],然后返回一个列表,列表里的元素是按照它们在 hand 中出现的顺序排列的 (而不是按照 positions 里的顺序)。

在每个空里填入以下名称之一:list, map, filter 等等。想了解这些函数的功能,可以参考内置函数列表方法的文档。

讨论时间:在开始写代码之前,小组一起讨论一下,用什么方法来确保正确地移除并返回卡牌。尽量别用猜和试的方法!讨论的目的是让大家在没有解释器帮忙检查的情况下,尝试解决问题。

def draw(hand, positions):
"""Remove and return the items at positions from hand.

>>> hand = ['A', 'K', 'Q', 'J', 10, 9]
>>> draw(hand, [2, 1, 4])
['K', 'Q', 10]
>>> hand
['A', 'J', 9]
"""
return _____(_____( [hand._____(i) for i in _____(_____(positions))] ))

在 61A 代码中运行

[提示:*]

对于列表 s 和整数 is.pop(i) 会返回并删除第 i 个元素。这会改变后面所有元素的位置(索引),但不会影响前面元素的位置。

对列表 s 调用 reversed(s) 会返回一个迭代器。 调用 list(reversed(s)) 会返回一个列表,其中包含 s 中元素的相反顺序。

搞定了?给自己鼓个掌!

面向对象编程

定义新类的一种有效方法是确定每个对象应该具有哪些实例属性,以及每个类应该具有哪些类属性。首先,描述一下每个属性的类型,以及它会怎么被用到。然后试着用这些属性来实现类的方法。

Q2:键盘

概述: 键盘的每个字母都有一个按钮。当按下按钮时,它会通过调用 output 函数(例如 print)来输出其字母。字母是大写还是小写,取决于 caps lock 键按了多少次。

首先,实现 Button 类,它接受一个小写 letter(一个字符串)和一个单参数 output 函数,例如 Button('c', print)

Button 的 press 方法会调用它的 output 属性 (一个函数),这个函数会作用于 letter 属性:如果 caps_lock 按了奇数次,就输出大写字母,否则输出小写字母。press 方法还会把 pressed 的值加一,并且返回按下的键。提示'hi'.upper() 的计算结果为 'HI'

其次,实现 Keyboard 类。Keyboard 有一个名为 keys 的字典,其中包含 Button(以其 letter 作为其键),用于 LOWERCASE_LETTERS 中的每个字母。它还有一个字母列表 typed,里面可能包含大写和小写字母。

type 方法接收一个字符串 word,这个字符串只包含小写字母。对于 word 里的每个字母,它会调用 keys 里对应 Buttonpress 方法。这会往 Keyboardtyped 列表里添加一个字母 (大写或小写,取决于 caps_lock 的状态)。 注意: 在实现 type 的时候,不要用 upper 或者 letter,直接调用 press 就行。

阅读 doctest 并讨论:

  • 为什么可以连续使用 .press().press().press() 来多次按下同一个按钮?
  • 为什么连续按按钮有时只在一行输出,有时却会输出多行?
  • 为什么 bored.typed 最后有 10 个元素?

讨论时间:在任何人开始编写代码之前,先进行一次对话,描述每个属性的类型以及它将如何使用。从 Button 开始:letteroutput 将如何使用?然后讨论 Keyboardtypedkeys 将如何使用?每次按下 keys 中的 Button 时,新的字母将如何添加到名为 typed 的列表中?如果不确定,请呼叫工作人员!一旦每个人都理解了这些问题的答案,你们就可以尝试一起编写代码了。

LOWERCASE_LETTERS = 'abcdefghijklmnopqrstuvwxyz'

class CapsLock:
def __init__(self):
self.pressed = 0

def press(self):
self.pressed += 1

class Button:
"""A button on a keyboard.

>>> f = lambda c: print(c, end='') # The end='' argument avoids going to a new line
>>> k, e, y = Button('k', f), Button('e', f), Button('y', f)
>>> s = e.press().press().press()
eee
>>> caps = Button.caps_lock
>>> t = [x.press() for x in [k, e, y, caps, e, e, k, caps, e, y, e, caps, y, e, e]]
keyEEKeyeYEE
>>> u = Button('a', print).press().press().press()
A
A
A
"""
caps_lock = CapsLock()

def __init__(self, letter, output):
assert letter in LOWERCASE_LETTERS
self.letter = letter
self.output = output
self.pressed = 0

def press(self):
"""Call output on letter (maybe uppercased), then return the button that was pressed."""
self.pressed += 1
"*** YOUR CODE HERE ***"

在 61A 代码中运行

在小组讨论并确认需要提示之前,请不要查看提示。

[提示:*]

因为 self.letter 总是小写,所以可以用 self.letter.upper() 得到大写版本。

caps_lock 键被按下的次数可以通过 self.caps_lock.pressedButton.caps_lock.pressed 来获取。

你不需要返回任何结果,只需返回 self 即可,这样就能返回被按下的按钮。

class Keyboard:
"""A keyboard.

>>> Button.caps_lock.pressed = 0 # Reset the caps_lock key
>>> bored = Keyboard()
>>> bored.type('hello')
>>> bored.typed
['h', 'e', 'l', 'l', 'o']
>>> bored.keys['l'].pressed
2

>>> Button.caps_lock.press()
>>> bored.type('hello')
>>> bored.typed
['h', 'e', 'l', 'l', 'o', 'H', 'E', 'L', 'L', 'O']
>>> bored.keys['l'].pressed
4
"""
def __init__(self):
self.typed = []
self.keys = ... # Try a dictionary comprehension!

def type(self, word):
"""Press the button for each letter in word."""
assert all([w in LOWERCASE_LETTERS for w in word]), 'word must be all lowercase'
"*** YOUR CODE HERE ***"
[提示:*]

可以使用字典推导式创建按键: self.keys = {c: Button(c, ...) for c in LETTERS}。对 Button 的调用应该接受 c一个将附加到 self.typed 的输出函数,以便每次按下这些按钮之一时,它都会将一个字母附加到此 Keyboardself.typed

对于 word 中的每一个字母 w,调用 self.keys[w]press 方法。理想情况下,在调用 press 方法时,Button 应该已经配置好(在 Keyboard.__init__ 方法中),能够将输出添加到 Keyboard 对象的 typed 列表中。

演示时间:描述每次按下 keys 中的 Button 时,如何将新字母添加到 typed。不要只是阅读你的代码,而是说它做了什么(例如,“当键盘的按钮被按下时……”)。一句话就足以描述如何将新字母添加到 typed。准备好后,向 #discuss-queue 频道发送一条消息,其中包含 @discuss 标签、你的讨论小组号码以及消息“Put it on our tab!”,课程工作人员将加入你的语音频道,听取你的描述并提供反馈。

Q3: 熊

实现 SleepyBearWinkingBear 类,以便调用它们的 print 方法与 doctest 匹配。使用尽可能少的代码,并尽量不要重复 EyeBear 中的任何逻辑。每个空白处只能填写两行简短的代码。 讨论环节: 在编写代码之前,讨论一下 SleepyBearBear 有什么不同。当使用继承时,你只需要实现子类与基类之间的差异。然后,讨论一下 WinkingBearBear 有什么不同。你们能想到不用重新实现 print 方法,也能让熊眨眼的方法吗?

class Eye:
"""An eye.

>>> Eye().draw()
'0'
>>> print(Eye(False).draw(), Eye(True).draw())
0 -
"""
def __init__(self, closed=False):
self.closed = closed

def draw(self):
if self.closed:
return '-'
else:
return '0'

class Bear:
"""A bear.

>>> Bear().print()
? 0o0?
"""
def __init__(self):
self.nose_and_mouth = 'o'

def next_eye(self):
return Eye()

def print(self):
left, right = self.next_eye(), self.next_eye()
print('? ' + left.draw() + self.nose_and_mouth + right.draw() + '?')

class SleepyBear(Bear):
"""A bear with closed eyes.

>>> SleepyBear().print()
? -o-?
"""
"*** YOUR CODE HERE ***"

class WinkingBear(Bear):
"""A bear whose left eye is different from its right eye.

>>> WinkingBear().print()
? -o0?
"""
def __init__(self):
"*** YOUR CODE HERE ***"

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

在 61A 代码编辑器中运行

实现一个 next_eye 方法,该方法返回一个已关闭的 Eye 实例。

一种让熊眨眼的方法是,使用一个新的实例属性来记录 next_eye 方法被调用的次数,如果 next_eye 方法被调用了偶数次,就返回一个闭着的眼睛。

记录出勤情况

请大家填写考勤表(每人每周提交一次)。