使用 Python 學習物件導向的程式設計

2019-09-08 09:11:00

使用 Python 類使你的程式碼變得更加模組化。

在我上一篇文章中,我解釋了如何通過使用函數、建立模組或者兩者一起來。函數對於避免重複多次使用的程式碼非常有用,而模組可以確保你在不同的專案中複用程式碼。但是模組化還有另一種方法:類。

如果你已經聽過物件導向程式設計object-oriented programming(OOP)這個術語,那麼你可能會對類的用途有一些概念。程式設計師傾向於將類視為一個虛擬物件,有時與物理世界中的某些東西直接相關,有時則作為某種程式設計概念的表現形式。無論哪種表示,當你想要在程式中為你或程式的其他部分建立“物件”時,你都可以建立一個類來互動。

沒有類的模板

假設你正在編寫一個以幻想世界為背景的遊戲,並且你需要這個應用程式能夠湧現出各種壞蛋來給玩家的生活帶來一些刺激。了解了很多關於函數的知識後,你可能會認為這聽起來像是函數的一個教科書案例:需要經常重複的程式碼,但是在呼叫時可以考慮變數而只編寫一次。

下面一個純粹基於函數的敵人生成器實現的例子:

#!/usr/bin/env python3import randomdef enemy(ancestry,gear):    enemy=ancestry    weapon=gear    hp=random.randrange(0,20)    ac=random.randrange(0,20)    return [enemy,weapon,hp,ac]def fight(tgt):    print("You take a swing at the " + tgt[0] + ".")    hit=random.randrange(0,20)    if hit > tgt[3]:        print("You hit the " + tgt[0] + " for " + str(hit) + " damage!")        tgt[2] = tgt[2] - hit    else:        print("You missed.")foe=enemy("troll","great axe")print("You meet a " + foe[0] + " wielding a " + foe[1])print("Type the a key and then RETURN to attack.")while True:    action=input()    if action.lower() == "a":        fight(foe)    if foe[2] < 1:        print("You killed your foe!")    else:        print("The " + foe[0] + " has " + str(foe[2]) + " HP remaining")

enemy 函數創造了一個具有多個屬性的敵人,例如譜系、武器、生命值和防禦等級。它返回每個屬性的列表,表示敵人全部特徵。

從某種意義上說,這段程式碼建立了一個物件,即使它還沒有使用類。程式設計師將這個 enemy 稱為物件,因為該函數的結果(本例中是一個包含字串和整數的列表)表示遊戲中一個單獨但複雜的東西。也就是說,列表中字串和整數不是任意的:它們一起描述了一個虛擬物件。

在編寫描述符集合時,你可以使用變數,以便隨時使用它們來生成敵人。這有點像模板。

在範例程式碼中,當需要物件的屬性時,會檢索相應的列表項。例如,要獲取敵人的譜系,程式碼會查詢 foe[0],對於生命值,會查詢 foe[2],以此類推。

這種方法沒有什麼不妥,程式碼按預期執行。你可以新增更多不同型別的敵人,建立一個敵人型別列表,並在敵人建立期間從列表中隨機選擇,等等,它工作得很好。實際上,Lua 非常有效地利用這個原理來近似了一個物件導向模型。

然而,有時候物件不僅僅是屬性列表。

使用物件

在 Python 中,一切都是物件。你在 Python 中建立的任何東西都是某個預定義模板的範例。甚至基本的字串和整數都是 Python type 類的衍生物。你可以在這個互動式 Python shell 中見證:

>>> foo=3>>> type(foo)<class 'int'>>>> foo="bar">>> type(foo)<class 'str'>

當一個物件由一個類定義時,它不僅僅是一個屬性的集合,Python 類具有各自的函數。從邏輯上講,這很方便,因為只涉及某個物件類的操作包含在該物件的類中。

在範例程式碼中,fight 的程式碼是主應用程式的功能。這對於一個簡單的遊戲來說是可行的,但對於一個複雜的遊戲來說,世界中不僅僅有玩家和敵人,還可能有城鎮居民、牲畜、建築物、森林等等,它們都不需要使用戰鬥功能。將戰鬥程式碼放在敵人的類中意味著你的程式碼更有條理,在一個複雜的應用程式中,這是一個重要的優勢。

此外,每個類都有特權存取自己的本地變數。例如,敵人的生命值,除了某些功能之外,是不會改變的資料。遊戲中的隨機蝴蝶不應該意外地將敵人的生命值降低到 0。理想情況下,即使沒有類,也不會發生這種情況。但是在具有大量活動部件的複雜應用程式中,確保不需要相互互動的部件永遠不會發生這種情況,這是一個非常有用的技巧。

Python 類也受垃圾收集的影響。當不再使用類的範例時,它將被移出記憶體。你可能永遠不知道這種情況會什麼時候發生,但是你往往知道什麼時候它不會發生,因為你的應用程式佔用了更多的記憶體,而且執行速度比較慢。將資料集隔離到類中可以幫助 Python 跟蹤哪些資料正在使用,哪些不在需要了。

優雅的 Python

下面是一個同樣簡單的戰鬥遊戲,使用了 Enemy 類:

#!/usr/bin/env python3import randomclass Enemy():    def __init__(self,ancestry,gear):        self.enemy=ancestry        self.weapon=gear        self.hp=random.randrange(10,20)        self.ac=random.randrange(12,20)        self.alive=True    def fight(self,tgt):        print("You take a swing at the " + self.enemy + ".")        hit=random.randrange(0,20)        if self.alive and hit > self.ac:            print("You hit the " + self.enemy + " for " + str(hit) + " damage!")            self.hp = self.hp - hit            print("The " + self.enemy + " has " + str(self.hp) + " HP remaining")        else:            print("You missed.")        if self.hp < 1:            self.alive=False# 遊戲開始foe=Enemy("troll","great axe")print("You meet a " + foe.enemy + " wielding a " + foe.weapon)# 主函數迴圈while True:       print("Type the a key and then RETURN to attack.")            action=input()    if action.lower() == "a":        foe.fight(foe)                    if foe.alive == False:        print("You have won...this time.")        exit()

這個版本的遊戲將敵人作為一個包含相同屬性(譜系、武器、生命值和防禦)的物件來處理,並新增一個新的屬性來衡量敵人時候已被擊敗,以及一個戰鬥功能。

類的第一個函數是一個特殊的函數,在 Python 中稱為 init 或初始化的函數。這類似於其他語言中的構造器,它建立了類的一個範例,你可以通過它的屬性和呼叫類時使用的任何變數來識別它(範例程式碼中的 foe)。

Self 和類範例

類的函數接受一種你在類之外看不到的新形式的輸入:self。如果不包含 self,那麼當你呼叫類函數時,Python 無法知道要使用的類的哪個範例。這就像在一間充滿獸人的房間裡說:“我要和獸人戰鬥”,向一個獸人發起。沒有人知道你指的是誰,所有獸人就都上來了。

Image of an Orc, CC-BY-SA by Buch on opengameart.org

CC-BY-SA by Buch on opengameart.org

類中建立的每個屬性都以 self 符號作為字首,該符號將變數標識為類的屬性。一旦派生出類的範例,就用表示該範例的變數替換掉 self 字首。使用這個技巧,你可以在一間滿是獸人的房間裡說:“我要和譜系是 orc 的獸人戰鬥”,這樣來挑戰一個獸人。當 orc 聽到 “gorblar.orc” 時,它就知道你指的是誰(他自己),所以你得到是一場公平的戰鬥而不是鬥毆。在 Python 中:

gorblar=Enemy("orc","sword")print("The " + gorblar.enemy + " has " + str(gorblar.hp) + " remaining.")

通過檢索類屬性(gorblar.enemygorblar.hp 或你需要的任何物件的任何值)而不是查詢 foe[0](在函數範例中)或 gorblar[0] 來尋找敵人。

本地變數

如果類中的變數沒有以 self 關鍵字作為字首,那麼它就是一個區域性變數,就像在函數中一樣。例如,無論你做什麼,你都無法存取 Enemy.fight 類之外的 hit 變數:

>>> print(foe.hit)Traceback (most recent call last):  File "./enclass.py", line 38, in <module>    print(foe.hit)AttributeError: 'Enemy' object has no attribute 'hit'>>> print(foe.fight.hit)Traceback (most recent call last):  File "./enclass.py", line 38, in <module>    print(foe.fight.hit)AttributeError: 'function' object has no attribute 'hit'

hit 變數包含在 Enemy 類中,並且只能“存活”到在戰鬥中發揮作用。

更模組化

本例使用與主應用程式相同的文字文件中的類。在一個複雜的遊戲中,我們更容易將每個類看作是自己獨立的應用程式。當多個開發人員處理同一個應用程式時,你會看到這一點:一個開發人員負責一個類,另一個開發人員負責主程式,只要他們彼此溝通這個類必須具有什麼屬性,就可以並行地開發這兩個程式碼塊。

要使這個範例遊戲模組化,可以把它拆分為兩個檔案:一個用於主應用程式,另一個用於類。如果它是一個更複雜的應用程式,你可能每個類都有一個檔案,或每個邏輯類組有一個檔案(例如,用於建築物的檔案,用於自然環境的檔案,用於敵人或 NPC 的檔案等)。

將只包含 Enemy 類的一個檔案儲存為 enemy.py,將另一個包含其他內容的檔案儲存為 main.py

以下是 enemy.py

import randomclass Enemy():    def __init__(self,ancestry,gear):        self.enemy=ancestry        self.weapon=gear        self.hp=random.randrange(10,20)        self.stg=random.randrange(0,20)        self.ac=random.randrange(0,20)        self.alive=True    def fight(self,tgt):        print("You take a swing at the " + self.enemy + ".")        hit=random.randrange(0,20)        if self.alive and hit > self.ac:            print("You hit the " + self.enemy + " for " + str(hit) + " damage!")            self.hp = self.hp - hit            print("The " + self.enemy + " has " + str(self.hp) + " HP remaining")        else:            print("You missed.")        if self.hp < 1:            self.alive=False

以下是 main.py

#!/usr/bin/env python3import enemy as en# game startfoe=en.Enemy("troll","great axe")print("You meet a " + foe.enemy + " wielding a " + foe.weapon)# main loopwhile True:       print("Type the a key and then RETURN to attack.")    action=input()    if action.lower() == "a":        foe.fight(foe)    if foe.alive == False:        print("You have won...this time.")        exit()

匯入模組 enemy.py 使用了一條特別的語句,參照類檔名稱而不用帶有 .py 擴充套件名,後跟你選擇的名稱空間指示符(例如,import enemy as en)。這個指示符是在你呼叫類時在程式碼中使用的。你需要在匯入時新增指示符,例如 en.Enemy,而不是只使用 Enemy()

所有這些檔名都是任意的,儘管在原則上不要使用罕見的名稱。將應用程式的中心命名為 main.py 是一個常見約定,和一個充滿類的檔案通常以小寫形式命名,其中的類都以大寫字母開頭。是否遵循這些約定不會影響應用程式的執行方式,但它確實使經驗豐富的 Python 程式設計師更容易快速理解應用程式的工作方式。

在如何構建程式碼方面有一些靈活性。例如,使用該範例程式碼,兩個檔案必須位於同一目錄中。如果你只想將類打包為模組,那麼必須建立一個名為 mybad 的目錄,並將你的類移入其中。在 main.py 中,你的 import 語句稍有變化:

from mybad import enemy as en

兩種方法都會產生相同的結果,但如果你建立的類足夠通用,你認為其他開發人員可以在他們的專案中使用它們,那麼後者更好。

無論你選擇哪種方式,都可以啟動遊戲的模組化版本:

$ python3 ./main.py You meet a troll wielding a great axeType the a key and then RETURN to attack.aYou take a swing at the troll.You missed.Type the a key and then RETURN to attack.aYou take a swing at the troll.You hit the troll for 8 damage!The troll has 4 HP remainingType the a key and then RETURN to attack.aYou take a swing at the troll.You hit the troll for 11 damage!The troll has -7 HP remainingYou have won...this time.

遊戲啟動了,它現在更加模組化了。現在你知道了物件導向的應用程式意味著什麼,但最重要的是,當你向獸人發起決鬥的時候,你知道是哪一個。