日拱一卒,伯克利教你學Python,面向物件和繼承
大家好,日拱一卒,我是梁唐。本文始發於公眾號Coder梁
今天分享的是伯克利大學CS61A公開課的第六節實驗課,這一次實驗的主要課題是面向物件。
和之前的相比,這一次的實驗內容更加出彩,質量也尤其高。不僅有生動有趣的例子,而且在題目之前也有充分的說明和演示。對於新手來說是一個絕好的學習和練習的機會。
如果想要了解或一下Python的,再次安利一下。
這一次用到的檔案有一點多,因為後面我們會開發一個文字冒險遊戲,這會需要用到幾個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
但make
和model
是怎麼儲存的呢?讓我們來談談例項屬性(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
。
我們的車還有其他的例項屬性,比如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
:
```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
不同!這是因為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
兩個類的完整定義:
```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.py
中Player
類的定義,在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),接著將它放入你的揹包。檢視程式碼,獲取更多細節。
提示:things
是Place
類的例項屬性,它將物品名稱和物件對映起來。
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'
你需要實現Key
和unlock
來通過測試:
```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,而不知道這個名稱對應的物件。這需要我們使用Place
的get_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
玩得開心!
- 日拱一卒,麻省理工教你效能分析,火焰圖、系統呼叫棧,黑科技滿滿
- 日拱一卒,麻省理工教你Debug,從此debug不再脫髮
- 日拱一卒,麻省理工教你學Git,工程師必備技能
- 日拱一卒,配置開發環境不再愁,麻省理工手把手教你
- 日拱一卒,麻省理工CS入門課,命令列這樣用也太帥了
- 日拱一卒,麻省理工YYDS,一節課讓我學會用vim
- 日拱一卒,麻省理工教你CS基礎,那些酷炫無比的命令列工具
- LeetCode周賽297,一小時AK你也可以
- 聽說你入行好幾年還只會cd和ls,麻省理工開了這門課……
- 日拱一卒,伯克利CS61A完結撒花
- 日拱一卒,伯克利教你用SQL處理資料
- LeetCode周賽296,難度較低的新手練習場
- 日拱一卒,伯克利教你Python核心技術,迭代器和生成器
- 日拱一卒,伯克利CS61A,教你用Python寫一個Python直譯器
- 日拱一卒,伯克利教你lisp,神一樣的程式語言
- LeetCode周賽295,賽後看了大佬的程式碼,受益匪淺
- 日拱一卒,伯克利的Python期中考試,你能通過嗎?
- 日拱一卒,伯克利教你資料結構
- 日拱一卒,伯克利教你學Python,面向物件和繼承
- LeetCode周賽294,硬核壓軸題的手速場