日拱一卒,伯克利教你學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

玩得開心!