使用 Python 學習面向對象的編程
在我上一篇文章中,我解釋了如何通過使用函數、創建模塊或者兩者一起來使 Python 代碼更加模塊化。函數對於避免重複多次使用的代碼非常有用,而模塊可以確保你在不同的項目中復用代碼。但是模塊化還有另一種方法:類。
如果你已經聽過 面向對象編程 (OOP)這個術語,那麼你可能會對類的用途有一些概念。程序員傾向於將類視為一個虛擬對象,有時與物理世界中的某些東西直接相關,有時則作為某種編程概念的表現形式。無論哪種表示,當你想要在程序中為你或程序的其他部分創建「對象」時,你都可以創建一個類來交互。
沒有類的模板
假設你正在編寫一個以幻想世界為背景的遊戲,並且你需要這個應用程序能夠湧現出各種壞蛋來給玩家的生活帶來一些刺激。了解了很多關於函數的知識後,你可能會認為這聽起來像是函數的一個教科書案例:需要經常重複的代碼,但是在調用時可以考慮變數而只編寫一次。
下面一個純粹基於函數的敵人生成器實現的例子:
#!/usr/bin/env python3
import random
def 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 python3
import random
class 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](/data/attachment/album/201909/08/091202o7lzrliwfprtwilt.jpg "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.enemy
或 gorblar.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 random
class 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 python3
import enemy as en
# game start
foe=en.Enemy("troll","great axe")
print("You meet a " + foe.enemy + " wielding a " + foe.weapon)
# main loop
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()
導入模塊 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 axe
Type the a key and then RETURN to attack.
a
You take a swing at the troll.
You missed.
Type the a key and then RETURN to attack.
a
You take a swing at the troll.
You hit the troll for 8 damage!
The troll has 4 HP remaining
Type the a key and then RETURN to attack.
a
You take a swing at the troll.
You hit the troll for 11 damage!
The troll has -7 HP remaining
You have won...this time.
遊戲啟動了,它現在更加模塊化了。現在你知道了面向對象的應用程序意味著什麼,但最重要的是,當你向獸人發起決鬥的時候,你知道是哪一個。
via: https://opensource.com/article/19/7/get-modular-python-classes
作者:Seth Kenlon 選題:lujun9972 譯者:MjSeven 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive