日拱一卒,伯克利教你学Python,面向对象和继承

语言: CN / TW / HK

大家好,日拱一卒,我是梁唐。本文始发于公众号Coder梁

今天分享的是伯克利大学CS61A公开课的第六节实验课,这一次实验的主要课题是面向对象。

和之前的相比,这一次的实验内容更加出彩,质量也尤其高。不仅有生动有趣的例子,而且在题目之前也有充分的说明和演示。对于新手来说是一个绝好的学习和练习的机会。

如果想要了解或一下Python的,再次安利一下。

课程链接

实验原始文档

Github

这一次用到的文件有一点多,因为后面我们会开发一个文字冒险游戏,这会需要用到几个python文件。下载完成之后,就可以准备开始进行实验了。

Object-Oriented Programming

面向对象编程。

在这次实验当中,我们将会深入面向对象编程(OOP),这是一种允许你将数据抽象成拥有各种属性和行为的实体的编程模式。这些抽象出来的实体就和真实世界中的实体一样,拥有种种特性,理解起来更加直观。

你可以从下面的文档当中阅读到更多的细节(如果你英文够好的话):http://composingprograms.com/pages/25-object-oriented-programming.html

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类型的构造函数看起来长这样:

python 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允许我们直接通过类名来创建。

```python

hilfingers_car = Car('Tesla', 'Model S') ```

这里,'Tesla'作为make传入,'Model S'作为model参数。注意,我们没有为self传入参数。这是因为它对应的值是要创建的对象。对象是类的一个实例。在这个例子当中,hilfingers_car现在绑定到了Car对象上,或者换句话说,Car类的一个实例。

Attributes

makemodel是怎么保存的呢?让我们来谈谈实例属性(attributes)和类属性。

下面是car.py文件中关于实例属性和类属性的一个代码片段:

```python 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

我们的车还有其他的实例属性,比如colorwheels。作为实例属性,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

```python

Car.size 'Tiny' ```

我们同样也可以这样访问:

```python

hilfingers_car.color 'No color yet. You need to paint me.' ```

Methods

让我们使用Car类的paint方法,方法是类中独有的函数,只有类的实例可以使用它们。我们已经看过了一个方法__init__。可以把方法想象成对象的能力或者是行为,我们怎么调用实例的方法呢?很显然,通过.操作。

```python

hilfingers_car.paint('black') 'Tesla Model S is now black' hilfingers_car.color 'black' ```

运行成功了,但如果你看一下paint方法的代码,会发现它接收了两个参数。但为什么我们不需要传入两个参数呢?

因为和__init__一样,所有的类方法都会自动传入self作为第一个参数。这里我们的hilfingers_car绑定了self,所以paint方法中访问到了它的属性。

你也可以通过类名和.标记调用方法,比如:

```python

Car.paint(hilfingers_car, 'red') 'Tesla Model S is now red' ```

注意这里我们需要传入两个参数,一个是self,一个是color。因为当我们通过实例调用方法的时候,Python会将实例自动作为self参数。然而,当我们通过类调用的时候,Python并不知道调用的是哪一个实例,所以我们需要手动传入。

Inheritance

继承

Hilfinger教授的红色Tesla太帅了,但遇到堵车依然抓瞎。不如我们给他创建一个大脚越野车吧。在car.py当中,我们创建了MonsterTruck类,让我们来看下它的代码:

```python 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教授创建一个大脚车:

```python

hilfingers_truck = MonsterTruck('Monster Truck', 'XXL') ```

它和你的预期一样吗?你还能为它上色吗?它还是可驾驶的吗?

MonsterTruck类被定义成了class MonsterTruck(Car):,这意味着这是Car的子类。也就是说MonsterTruck类继承了Car中所有的属性和方法,包括构造函数!

继承创建了类之间的层次关系,使得我们创建类的时候可以节省很大一波代码。你只需要创建(或重载)子类所独有的新属性或方法。

```python

hilfingers_car.size 'Tiny' hilfingers_truck.size 'Monster' ```

它们的size不同!这是因为MonsterTrucksize覆盖了Car中的size,所以MonsterTruck类的实例拥有的都是Monstersize

特别的,MonsterTruckdrive方法也覆盖了Car的。为了展示MonsterTruck实例,我们为MonsterClass专门定义了一个rev方法,而普通的Car不能rev。而其他的部分都是继承子Car类型。

Required Questions

WWPD

Q1: Using the Car class

下面是car.pyCarMonsterTruck两个类的完整定义:

```python 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代码回答输出结果:

bash python3 ok -q car -u python3 ok -q food_truck -u

如果代码运行报错,输入Error,如果什么也不会返回,输入Nothing。

一共31题,题目并不难,都是关于上面讲述的面向对象概念的一些简单应用。如果被某题难住了,粘贴到Python解释器中运行一下即可。

```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.pyPlayer类的定义,在data.py底部创建一个Player对象。

Player构造函数接收两个参数:

  • name是你喜欢的名字(string类型)
  • 开始的位置place

你的玩家将会从sather_gate开始

使用ok命令来进行测试:python3 ok -q me

代码很简单只有一行:

python me = Player('liangtang', sather_gate)

当你创建完成之后,就可以启动游戏了:python3 adventure.py

你会看到下面这些输出(可能顺序有所不同):

你现在除了look什么都做不了,让我们来继续完善它!

Q3: Where do I go?

首先,我们需要能够移动到不同的地方。如果你试着使用go to命令,你会发现什么也没有发生。

classes.py中,完成Player类的go_to方法,让没有阻碍的情况下,它能够将你的place属性更新成destination_place,并且你还需要输出当前位置的名称。

代码框架:

```python 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分支即可。

```python 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入参的函数。这个函数将会循环遍历零食,按顺序返回其中一个元素。

```python 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

答案

高阶函数的简单应用,我们用一个外部变量记录一下当前要返回的下标。由于不能再返回之后再执行语句,所以只能先记录下要返回的内容, 再修改下标。

python 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类中的什么方法来进行交谈呢?

```python 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

答案

```python 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),接着将它放入你的背包。查看代码,获取更多细节。

提示:thingsPlace类的实例属性,它将物品名称和对象映射起来。

Place类中的take方法也能派上用场。

```python 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需要删除掉对应项,不然会导致重复获取。

python 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实例属性,你可能需要改动它

```python 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'

你需要实现Keyunlock来通过测试:

```python 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,而不知道这个名称对应的对象。这需要我们使用Placeget_neighbour函数。

完整代码如下:

```python 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

玩得开心!