日拱一卒,伯克利教你学Python,面向对象和继承
大家好,日拱一卒,我是梁唐。本文始发于公众号Coder梁
今天分享的是伯克利大学CS61A公开课的第六节实验课,这一次实验的主要课题是面向对象。
和之前的相比,这一次的实验内容更加出彩,质量也尤其高。不仅有生动有趣的例子,而且在题目之前也有充分的说明和演示。对于新手来说是一个绝好的学习和练习的机会。
如果想要了解或一下Python的,再次安利一下。
这一次用到的文件有一点多,因为后面我们会开发一个文字冒险游戏,这会需要用到几个python文件。下载完成之后,就可以准备开始进行实验了。
Object-Oriented Programming
面向对象编程。
在这次实验当中,我们将会深入面向对象编程(OOP),这是一种允许你将数据抽象成拥有各种属性和行为的实体的编程模式。这些抽象出来的实体就和真实世界中的实体一样,拥有种种特性,理解起来更加直观。
你可以从下面的文档当中阅读到更多的细节(如果你英文够好的话):composingprograms.com/pages/25-ob…
OOP Example: Car Class
面向对象编程的例子:Car类
Hilfinger教授要迟到了,他需要在课程开始之前从旧金山到伯克利。
他想要乘坐BART(Bay Area Rapid Transit类似地铁),但这太慢了。这个时候如果有辆车就太好了,如果是大脚越野车(monster truck如下图)就更好了,但目前来说普通的车也行……
在car.py
中,你将会找到一个叫做Car
的类(class)。类是用来创建特定类型对象的蓝本。在这个例子当中,Car
类型告诉我们如何创建Car
对象。
Constructor
让我们给Hilfinger教授创建一辆车!别担心,你并不需要做什么苦力活——构造函数将会替你完成。
类中的构造函数是一种特殊的函数,用来创建对应类的实例(instance)。在Python当中,构造函数叫做__init__
。注意,init
单词的前后都有两个下划线,Car
类型的构造函数看起来长这样:
def __init__(self, make, model):
self.make = make
self.model = model
self.color = 'No color yet. You need to paint me.'
self.wheels = Car.num_wheels
self.gas = Car.gas
Car
类型的__init__
方法拥有三个参数,第一个参数是self
,这会自动绑定到新创建的Car
对象的实例上。第二和第三个参数make
,model
会绑定到传入构造函数的参数上,意味着当我们创建Car
对象时,我们只需要传入两个参数,目前为止不需要过多关心构造函数中的代码。
让我们创建我们的的车,Hifinger教授想要驾驶一辆特斯拉Model S来上课。我们可以用'Tesla'
作为make
,'Model S'
作为model
来创建Car
实例。相比于显式地调用__init__
函数,Python允许我们直接通过类名来创建。
>>> hilfingers_car = Car('Tesla', 'Model S')
这里,'Tesla'
作为make
传入,'Model S'
作为model
参数。注意,我们没有为self
传入参数。这是因为它对应的值是要创建的对象。对象是类的一个实例。在这个例子当中,hilfingers_car
现在绑定到了Car
对象上,或者换句话说,Car
类的一个实例。
Attributes
但make
和model
是怎么保存的呢?让我们来谈谈实例属性(attributes)和类属性。
下面是car.py
文件中关于实例属性和类属性的一个代码片段:
class Car(object):
num_wheels = 4
gas = 30
headlights = 2
size = 'Tiny'
def __init__(self, make, model):
self.make = make
self.model = model
self.color = 'No color yet. You need to paint me.'
self.wheels = Car.num_wheels
self.gas = Car.gas
def paint(self, color):
self.color = color
return self.make + ' ' + self.model + ' is now ' + color
在构造函数的头两行,self.make
绑定在了构造函数的第一个入参上,而self.model
绑定了第二个入参上。这是实例属性的两个例子。
实例属性是实例所独有的,所有我们只能通过实例名加上.
符号来访问。self
绑定到了我们创建的实例上,所以self.model
就代表了我们的实例属性model
。
我们的车还有其他的实例属性,比如color
和wheels
。作为实例属性,make, model, color
不会影响其他车的make, model, color
。
但类属性就不是这么回事了,类熟悉了是被类中所有实例共享的值。举个例子,Car
这个类拥有4个类属性,定义在了类的开头:num_wheels=4, gas=30, headlights=2, size='Tiny'
。
你可能已经注意到了,在构造函数当中,我们的实例属性gas
初始化成了类属性Car.gas
。为什么我们不直接使用类属性呢?因为每辆车的gas
都是不同的。开了一段时间的车gas
会减少是正常的,但如果车还没开gas
就减少了,显然就有问题了。而直接修改类属性会导致影响其他的实例,所以这里我们不能用类属性,而只能用实例属性。
Dot Notation
类属性同样使用.
来访问,既可以用类名也可以用实例名访问。比如说,我们可以通过以下方式访问类属性size
:
>>> Car.size
'Tiny'
我们同样也可以这样访问:
>>> hilfingers_car.color
'No color yet. You need to paint me.'
Methods
让我们使用Car
类的paint
方法,方法是类中独有的函数,只有类的实例可以使用它们。我们已经看过了一个方法__init__
。可以把方法想象成对象的能力或者是行为,我们怎么调用实例的方法呢?很显然,通过.
操作。
>>> hilfingers_car.paint('black')
'Tesla Model S is now black'
>>> hilfingers_car.color
'black'
运行成功了,但如果你看一下paint
方法的代码,会发现它接收了两个参数。但为什么我们不需要传入两个参数呢?
因为和__init__
一样,所有的类方法都会自动传入self
作为第一个参数。这里我们的hilfingers_car
绑定了self
,所以paint
方法中访问到了它的属性。
你也可以通过类名和.
标记调用方法,比如:
>>> Car.paint(hilfingers_car, 'red')
'Tesla Model S is now red'
注意这里我们需要传入两个参数,一个是self
,一个是color
。因为当我们通过实例调用方法的时候,Python会将实例自动作为self
参数。然而,当我们通过类调用的时候,Python并不知道调用的是哪一个实例,所以我们需要手动传入。
Inheritance
继承
Hilfinger教授的红色Tesla太帅了,但遇到堵车依然抓瞎。不如我们给他创建一个大脚越野车吧。在car.py
当中,我们创建了MonsterTruck
类,让我们来看下它的代码:
class MonsterTruck(Car):
size = 'Monster'
def rev(self):
print('Vroom! This Monster Truck is huge!')
def drive(self):
self.rev()
return Car.drive(self)
这个车非常大,但代码却很简单。
让我们来确认一下,它的功能符合预期。让我们为Hilfinger教授创建一个大脚车:
>>> hilfingers_truck = MonsterTruck('Monster Truck', 'XXL')
它和你的预期一样吗?你还能为它上色吗?它还是可驾驶的吗?
MonsterTruck
类被定义成了class MonsterTruck(Car):
,这意味着这是Car
的子类。也就是说MonsterTruck
类继承了Car
中所有的属性和方法,包括构造函数!
继承创建了类之间的层次关系,使得我们创建类的时候可以节省很大一波代码。你只需要创建(或重载)子类所独有的新属性或方法。
>>> hilfingers_car.size
'Tiny'
>>> hilfingers_truck.size
'Monster'
它们的size
不同!这是因为MonsterTruck
的size
覆盖了Car
中的size
,所以MonsterTruck
类的实例拥有的都是Monster
的size
。
特别的,MonsterTruck
的drive
方法也覆盖了Car
的。为了展示MonsterTruck
实例,我们为MonsterClass
专门定义了一个rev
方法,而普通的Car
不能rev
。而其他的部分都是继承子Car
类型。
Required Questions
WWPD
Q1: Using the Car class
下面是car.py
中Car
和MonsterTruck
两个类的完整定义:
class Car(object):
num_wheels = 4
gas = 30
headlights = 2
size = 'Tiny'
def __init__(self, make, model):
self.make = make
self.model = model
self.color = 'No color yet. You need to paint me.'
self.wheels = Car.num_wheels
self.gas = Car.gas
def paint(self, color):
self.color = color
return self.make + ' ' + self.model + ' is now ' + color
def drive(self):
if self.wheels < Car.num_wheels or self.gas <= 0:
return self.make + ' ' + self.model + ' cannot drive!'
self.gas -= 10
return self.make + ' ' + self.model + ' goes vroom!'
def pop_tire(self):
if self.wheels > 0:
self.wheels -= 1
def fill_gas(self):
self.gas += 20
return self.make + ' ' + self.model + ' gas level: ' + str(self.gas)
class MonsterTruck(Car):
size = 'Monster'
def rev(self):
print('Vroom! This Monster Truck is huge!')
def drive(self):
self.rev()
return Car.drive(self)
使用ok命令来根据Python代码回答输出结果:
python3 ok -q car -u
python3 ok -q food_truck -u
如果代码运行报错,输入Error,如果什么也不会返回,输入Nothing。
一共31题,题目并不难,都是关于上面讲述的面向对象概念的一些简单应用。如果被某题难住了,粘贴到Python解释器中运行一下即可。
>>> hilfingers_car = Car('Tesla', 'Model S')
>>> hilfingers_car.color
______
>>> hilfingers_car.paint('black')
______
>>> hilfingers_car.color
______
>>> hilfingers_car = Car('Tesla', 'Model S')
>>> hilfingers_truck = MonsterTruck('Monster Truck', 'XXL')
>>> hilfingers_car.size
______
>>> hilfingers_truck.size
______
>>> hilfingers_car = Car('Tesla', 'Model S')
>>> hilfingers_car.model
______
>>> hilfingers_car.gas = 10
>>> hilfingers_car.drive()
______
>>> hilfingers_car.drive()
______
>>> hilfingers_car.fill_gas()
______
>>> hilfingers_car.gas
______
>>> Car.gas
______
>>> Car.headlights
______
>>> hilfingers_car.headlights
______
>>> Car.headlights = 3
>>> hilfingers_car.headlights
______
>>> hilfingers_car.headlights = 2
>>> Car.headlights
______
>>> hilfingers_car.wheels = 2
>>> hilfingers_car.wheels
______
>>> Car.num_wheels
______
>>> hilfingers_car.drive()
______
>>> Car.drive()
______
>>> Car.drive(hilfingers_car)
______
>>> MonsterTruck.drive(hilfingers_car)
______
>>> deneros_car = MonsterTruck('Monster', 'Batmobile')
>>> deneros_car.drive()
______
>>> Car.drive(deneros_car)
______
>>> MonsterTruck.drive(deneros_car)
______
>>> Car.rev(deneros_car)
______
>>> class FoodTruck(MonsterTruck):
... delicious = 'meh'
... def serve(self):
... if FoodTruck.size == 'delicious':
... print('Yum!')
... if self.food != 'Tacos':
... return 'But no tacos...'
... else:
... return 'Mmm!'
>>> taco_truck = FoodTruck('Tacos', 'Truck')
>>> taco_truck.food = 'Guacamole'
>>> taco_truck.serve()
______
>>> taco_truck.food = taco_truck.make
>>> FoodTruck.size = taco_truck.delicious
>>> taco_truck.serve()
______
>>> taco_truck.size = 'delicious'
>>> taco_truck.serve()
______
>>> FoodTruck.pop_tire()
______
>>> FoodTruck.pop_tire(taco_truck)
______
>>> taco_truck.drive()
______
Adventure Game!
在这个实验的下一个部分,我们将会实现一个基于文本的冒险游戏。你可以通过输入python3 adventure.py
来开始游戏。
通过命令Ctrl-C
或者Ctrl-D
退出游戏。
Q2: Who am I?
首先,你需要为你自己在data.py
中创建一个Player
对象。看一下classes.py
中Player
类的定义,在data.py
底部创建一个Player
对象。
Player
构造函数接收两个参数:
name
是你喜欢的名字(string类型)- 开始的位置
place
你的玩家将会从sather_gate
开始
使用ok命令来进行测试:python3 ok -q me
代码很简单只有一行:
me = Player('liangtang', sather_gate)
当你创建完成之后,就可以启动游戏了:python3 adventure.py
你会看到下面这些输出(可能顺序有所不同):
你现在除了look什么都做不了,让我们来继续完善它!
Q3: Where do I go?
首先,我们需要能够移动到不同的地方。如果你试着使用go to
命令,你会发现什么也没有发生。
在classes.py
中,完成Player
类的go_to
方法,让没有阻碍的情况下,它能够将你的place
属性更新成destination_place
,并且你还需要输出当前位置的名称。
代码框架:
def go_to(self, location):
"""Go to a location if it's among the exits of player's current place.
>>> sather_gate = Place('Sather Gate', 'Sather Gate', [], [])
>>> gbc = Place('GBC', 'Golden Bear Cafe', [], [])
>>> sather_gate.add_exits([gbc])
>>> sather_gate.locked = True
>>> gbc.add_exits([sather_gate])
>>> me = Player('player', sather_gate)
>>> me.go_to('GBC')
You are at GBC
>>> me.place is gbc
True
>>> me.place.name
'GBC'
>>> me.go_to('GBC')
Can't go to GBC from GBC.
Try looking around to see where to go.
You are at GBC
>>> me.go_to('Sather Gate')
Sather Gate is locked! Go look for a key to unlock it
You are at GBC
"""
destination_place = self.place.get_neighbor(location)
if destination_place.locked:
print(destination_place.name, 'is locked! Go look for a key to unlock it')
"*** YOUR CODE HERE ***"
使用ok命令来进行测试python3 ok -q Player.go_to
当你完成了这个问题之后,你将可以移动到不同的位置并且进行查看(look)。为了完善游戏的功能,包括和NPC交谈以及捡起东西,你需要完成可选问题中的5-8题。
答案
很简单,代码里已经替我们写好了判断block的情况,我们只需要加上else分支即可。
def go_to(self, location):
destination_place = self.place.get_neighbor(location)
if destination_place.locked:
print(destination_place.name, 'is locked! Go look for a key to unlock it')
else:
self.place = destination_place
print('You are at {}'.format(self.place.name))
Optional Questions
Nonlocal Practice
Q4: Vending Machine
实现函数vending_machine
,它接收一个零食的list,返回一个0入参的函数。这个函数将会循环遍历零食,按顺序返回其中一个元素。
def vending_machine(snacks):
"""Cycles through sequence of snacks.
>>> vender = vending_machine(('chips', 'chocolate', 'popcorn'))
>>> vender()
'chips'
>>> vender()
'chocolate'
>>> vender()
'popcorn'
>>> vender()
'chips'
>>> other = vending_machine(('brownie',))
>>> other()
'brownie'
>>> vender()
'chocolate'
"""
"*** YOUR CODE HERE ***"
使用ok命令进行测试:python3 ok -q vending_machine
答案
高阶函数的简单应用,我们用一个外部变量记录一下当前要返回的下标。由于不能再返回之后再执行语句,所以只能先记录下要返回的内容, 再修改下标。
def vending_machine(snacks):
i = 0
def func():
nonlocal i
ret = snacks[i]
i = (i + 1) % len(snacks)
return ret
return func
More Adventure!
Q5: How do I talk?
现在你已经可以去你想去的地方了,试着去往Wheeler。在那里你可以找到Jerry。通过talk to
命令和它对话。这现在仍然不会生效。
接着,实现Player
中的talk_to
方法。talk_to
接收一个角色(Character)的名称,并且打印出它的反应。查看下面代码获取更多细节。
提示:talk_to
接收一个参数person
,它是一个字符串。self.place
中的实例属性characters
是一个字典,将角色的名称和角色的对象映射起来
当你拿到角色对象之后,你需要用Character
类中的什么方法来进行交谈呢?
def talk_to(self, person):
"""Talk to person if person is at player's current place.
>>> jerry = Character('Jerry', 'I am not the Jerry you are looking for.')
>>> wheeler = Place('Wheeler', 'You are at Wheeler', [jerry], [])
>>> me = Player('player', wheeler)
>>> me.talk_to(jerry)
Person has to be a string.
>>> me.talk_to('Jerry')
Jerry says: I am not the Jerry you are looking for.
>>> me.talk_to('Tiffany')
Tiffany is not here.
"""
if type(person) != str:
print('Person has to be a string.')
"*** YOUR CODE HERE ***"
使用ok命令来进行测试:python3 ok -q Player.talk_to
答案
def talk_to(self, person):
if type(person) != str:
print('Person has to be a string.')
else:
if person not in self.place.characters:
print('{} is not here.'.format(person))
return
character = self.place.characters[person]
print('{} says: {}'.format(person, character.talk()))
Q6: How do I take items?
现在让我们实现take
命令,让玩家可以往backpack
中放入道具。目前,你没有背包(backpack),所以让我们创建一个实例变量backpack
,将它初始化成空的list。
当你初始化你的空背包之后,实现take
方法,它接收一个物品的名称,检查你所在的位置是否有这件道具(Thing),接着将它放入你的背包。查看代码,获取更多细节。
提示:things
是Place
类的实例属性,它将物品名称和对象映射起来。
Place
类中的take
方法也能派上用场。
def take(self, thing):
"""Take a thing if thing is at player's current place
>>> lemon = Thing('Lemon', 'A lemon-looking lemon')
>>> gbc = Place('GBC', 'You are at Golden Bear Cafe', [], [lemon])
>>> me = Player('Player', gbc)
>>> me.backpack
[]
>>> me.take(lemon)
Thing should be a string.
>>> me.take('orange')
orange is not here.
>>> me.take('Lemon')
Player takes the Lemon
>>> me.take('Lemon')
Lemon is not here.
>>> isinstance(me.backpack[0], Thing)
True
>>> len(me.backpack)
1
"""
if type(thing) != str:
print('Thing should be a string.')
"*** YOUR CODE HERE ***"
使用ok命令来测试:python3 ok -q Player.take
答案
逻辑和上一题类似,没什么难度,注意获取东西之后,Place
中的things
需要删除掉对应项,不然会导致重复获取。
def take(self, thing):
if type(thing) != str:
print('Thing should be a string.')
else:
if thing in self.place.things:
item = self.place.things[thing]
self.backpack.append(item)
print('Player takes the {}'.format(thing))
self.place.things.pop(thing)
else:
print('{} is not here.'.format(thing))
Q7: No door can hold us back!
FSM锁上了,我们没有办法进去。而你已经对甜美可口的咖啡因非常绝望了。
为了进入FSM并且修复咖啡因,我们需要做两件事情。首先,我们需要创建一个新的类型Key
,它是Thing
的子类,但重载了use
方法来打开FSM的门。
提示1:如果对重载概念不清楚,可以回顾一下前面大脚车的例子
提示2:Place有一个locked
实例属性,你可能需要改动它
class Thing(object):
def __init__(self, name, description):
self.name = name
self.description = description
def use(self, place):
print("You can't use a {0} here".format(self.name))
""" Implement Key here! """
你还需要完成Player
中的unlock
函数。它接收一个字符串place
,表示你想要打开的地方。如果你拥有钥匙(key),调用它的use
方法可以打开这个地方。如果你没有要是,那么这个方法会输出the place can't be unlocked without a key'
你需要实现Key
和unlock
来通过测试:
def unlock(self, place):
"""If player has a key, unlock a locked neighboring place.
>>> key = Key('SkeletonKey', 'A Key to unlock all doors.')
>>> gbc = Place('GBC', 'You are at Golden Bear Cafe', [], [key])
>>> fsm = Place('FSM', 'Home of the nectar of the gods', [], [])
>>> gbc.add_exits([fsm])
>>> fsm.locked = True
>>> me = Player('Player', gbc)
>>> me.unlock(fsm)
Place must be a string
>>> me.go_to('FSM')
FSM is locked! Go look for a key to unlock it
You are at GBC
>>> me.unlock(fsm)
Place must be a string
>>> me.unlock('FSM')
FSM can't be unlocked without a key!
>>> me.take('SkeletonKey')
Player takes the SkeletonKey
>>> me.unlock('FSM')
FSM is now unlocked!
>>> me.unlock('FSM')
FSM is already unlocked!
>>> me.go_to('FSM')
You are at FSM
"""
if type(place) != str:
print("Place must be a string")
return
key = None
for item in self.backpack:
if type(item) == Key:
key = item
"*** YOUR CODE HERE ***"
使用ok来进行测试:python3 ok -q Player.unlock
答案
其实逻辑很简单,有一个坑点是我们拿到的是要开启的地点的string,而不知道这个名称对应的对象。这需要我们使用Place
的get_neighbour
函数。
完整代码如下:
def unlock(self, place):
if type(place) != str:
print("Place must be a string")
return
key = None
for item in self.backpack:
if type(item) == Key:
key = item
"*** YOUR CODE HERE ***"
if key is None:
print("{} can't be unlocked without a key!".format(place))
return
next_place = self.place.get_neighbor(place)
if next_place.locked == False:
print(place, 'is already unlocked!')
return
key.use(next_place)
print(place, 'is now unlocked!')
class Key(Thing):
def use(self, place):
place.locked = False
Q8: Win the game!
现在你可以在校园里到处走动以及尝试着赢得游戏了。和各个地方的人交谈来获取提示。你能拯救这一天并且赶上61A的课程聚会吗?
python3 adventure.py
玩得开心!
转载自:https://juejin.cn/post/7101191276472565791