物件導向程式設計 - 入門篇
「物件導向程式設計(Object-Oriented Programming, OOP)」這個名詞聽起來好像很厲害,但它跟前面函數章節曾經提過的「函數式程式設計」一樣,都是一種程式設計的範式(Paradigm),OOP 主要是藉由「物件」這種擬人、擬物化的方式,用我們比較容易想像的方式來組織、管理程式碼。
什麼是物件 ?
這個問題如果問不同行業的人會有不同的答案,例如對於房仲業的從業人員來說,物件指的可能是一棟房子,但對軟體工程師來說,物件通常指的是一小塊記憶體位置,裡面包含資料和程式碼的資料結構。
在現實生活中,路上跑的車子、天上飛的鳥,同樣都是人類的你我,這種看得到、摸得到的都可通稱為物件,白話的說,物件就是「東西(Thing)」。物件會有「狀態」跟「行為」,例如我本人有「黑色頭髮」、「黃色皮膚」、「年紀 18 歲(?)」等狀態,也有「吃飯」、「睡覺」、「走路」、「講話」等行為。
物件就是一個東西,這個東西身上會有一些狀態跟行為:
- 狀態:髮色、膚色、年齡、身體、體重...等名詞
- 行為:走路、跑步、吃飯、講話、睡覺、追劇、打電動、寫程式...等動詞
所以:
物件(Object) = 狀態(State) + 行為(Behavior)
狀態,就是物件的「屬性(Attribute)」,而行為就是物件的「方法(Method)」,也就是寫在物件裡的函數。
為了讓大家更容易理解、學習程式,許多程式語言都有引進「擬人化」或「擬物化」的設計,讓我們可以用一般人更容易想像的方式來組織或管理程式碼,這就是所謂的物件導向程式設計。問題是,我們怎麼在 Python 建立或使用物件?這就得先跟大家介紹一下「類別(Class)」。
類別與物件
我記得我以前自 學程式的時候,每次看書上講到關於物件導向的介紹,常會說類別就像建築物或是飛機的「藍圖」一樣,透過藍圖就能建立很多物件。但我不會蓋房子也不會造飛機,這藍圖的比喻對我來說有點難想像,所以我改用我們這種平民老百姓更容易遇到、更生活化的範例。不知道大家有沒有在路邊或夜市看過或吃過雞蛋糕,有小貓、小狗、皮卡丘等可愛動物造型,只要把調配好的麵粉糊倒進烤盤,蓋上蓋子,等幾分鐘就有香噴噴又造型可愛的雞蛋糕可以吃了:
photo by Bryan Liu
以物件導向的專有名詞來說,這個烤盤模具,就是「類別(Class)」的概念。一樣形狀的模具,放一樣的原料進去,如果沒壓壞的話做出來雞蛋糕的形狀就都會長得一樣。透過烤盤做出來的雞蛋糕,以物件導向程式設計的專有名詞來說會稱之「實體(Instance)」,接下來的章節我都會使用這些名詞。
在 Python 定義類別,使用的關鍵字是 class
:
class Cat:
pass
因為目前還不知道要放什麼內容,所以我先放 pass
卡個位。這樣我們就定義了一個名為 Cat
的類別,不過目前這個類別裡面空空的,沒什麼用途就是了。
建立物件
有類別之後,就可以透過它來產生物件:
kitty = Cat()
nancy = Cat()
這就跟用烤盤模具可以一直做出雞蛋糕一樣,透過 Cat()
類別你想做幾隻貓都沒問題,這裡我先做了兩個 Cat
的實體,分別叫做 kitty
和 nancy
。雖然這兩個物件都是用 Cat
類別生成的,但它們是不同的物件,就像你跟我一樣雖然都是 人類
這個類別產生的實體,但我們都是不同的個體,雖然有相同的行為,但各自有不同的狀態。
看到這裡,應該也不難發現類別用起來的手感跟呼叫函數有點像。我們可以來看看這些東西長什麼樣子:
>>> kitty
<__main__.Cat object at 0x104c6d160>
>>> nancy
<__main__.Cat object at 0x104c6f0b0>
後面那個 0x104c6d160
或是 0x104c6f0b0
這種看起來像隨機英文數字組合的東西,它代表這顆物件所在的記憶體位置。這兩個數值不一樣,表示這兩顆物件所在的記憶體位置是不一樣的,也就是說,它們是兩個不同且獨立的個體。
雖然 kitty
跟 nancy
是不同的個體,但都是一種 Cat
:
>>> isinstance(kitty, Cat)
True
>>> isinstance(nancy, Cat)
True
透過內建函數 isinstance()
可以判斷物件是否為某個類別的實體,這裡我們可以看到 kitty
和 nancy
都是 Cat
類別的實體,但不是字串 str
的實體:
>>> isinstance(nancy, str)
False
另外,每個物件,不管是 我們自己設計的或是 Python 內建的,一定都是某個類別生出來的,沒有例外。透過 __class__
屬性可以觀察到這顆物件是由哪個類別產生的:
>>> kitty.__class__
<class '__main__.Cat'>
>>> kitty.__class__.__name__
'Cat'
再補個 __name__
屬性,就可以看到這個類別的名字。
初始化與實體屬性
物件如果只是個空殼那就太無聊了,就像雞蛋糕裡 沒有塞任何餡料一樣,所以我們可以試著在建立這些小貓物件的時候,順便幫牠們加點料:
kitty = Cat("凱蒂", "白色", 18)
nancy = Cat("南茜", "黑色", 20)
這裡我想幫這兩個小貓物件加上名字、顏色以及年紀,這樣就能透過屬性來區分這兩隻貓。不過這樣寫會出錯:
Traceback (most recent call last):
File "/demo/cat.py", line 5, in <module>
kitty = Cat("凱蒂", "白色", 18)
^^^^^^^^^^^^^^^^^^^^^^^
TypeError: Cat() takes no arguments
的確,我們在定義 Cat
類別的時候沒有設定任何參數所以 Python 會給這樣的錯誤訊息也是合理的。不過要解決這個問題不能直接對 Cat
類別本身下手,而是在這個類別裡定義一個名字有點特別的函數 __init__()
:
class Cat:
def __init__(name, color, age):
pass
當我們在 Python 裡透過類別建立一個實體的時候,定義在這個類別裡的 __init__()
函數就會自動被呼叫來進行「初始化」的行為,這也是這個函數名稱的由來。這個方法的名稱是固定的寫法,在 init
的前後各都有兩個底線,寫錯字或是底線數量不對就沒效果了。
當我們執行 Cat("凱蒂", "白色", 18)
的時候,Python 會先使用 Cat
類別建立一個實體,接著在實體建立完成之後馬上跟著執行 __init__()
函數,並且把剛剛收到的參數全部丟給它。參數雖然補上去了,但這樣執行之後還是會出錯:
Traceback (most recent call last):
File "/demo/cat.py", line 6, in <module>
kitty = Cat("凱蒂", "白色", 18)
^^^^^^^^^^^^^^^^^^^^^^^
TypeError: Cat.__init__() takes 3 positional arguments but 4 were given
錯誤訊息告訴我們 __init__()
函數只接受 3 個參數,但 Python 卻說我們給了 4 個。咦?有嗎?我應該沒眼花,我們這裡只給了 3 個不是嗎?事實上,這個 __init__()
函數有一個沒有那麼明顯的參數,就是自己這顆物件本身。所以 __init__()
需要調整成這樣:
class Cat:
def __init__(this_is_me, name, color, age):
pass
這樣再次執行應該就不會出錯了。關於 __init__()
函數的第一個參數,有些網路上的教學資料會跟你說第一個參數要放 self
,但這裡我先故意用 this_is_me
,目的就是跟大家說這個參數的名字你想取什麼名字都可以,self
並不是什麼特別的變數或設定,它就只是個參數名稱而已。雖然不是很明顯,但這個參數指的就是自己這顆實體本身,所以通常才會叫它 self
。我也不要太標新立異,接下來我還是會順著大家的慣例把它改成 self