約維安計畫:Python 的類別

第十七週。

我們在約維安計畫:Python 的函數認識了函數的觀念,函數是一段「被命名」的程式碼,這段程式碼可以用於執行某個特定任務,可能是數值的運算或者文字的處理。把某項能夠運作特定功能的程式碼「命名」的機制,稱之為組織程式碼,因應不同目的以及應用場景, 程式設計師有組織程式碼的需求,希望可以簡潔且有效率地完成任務,具體來說,就是提升程式碼的重複利用性(Reusability)。事實上,除了函數以外,Python 與大多數的程式語言相同,擁有其他組織程式碼的機制,具體而言有三個層次,由小而大依序為:

  1. 函數(Functions)。

  2. 類別(Classes)。

  3. 模組(Modules)。

本篇文章要介紹組織程式碼機制的第二個層次:類別

什麼是類別

把即將要執行的程式碼組織為函數,並依序使用這些函數來完成任務,這是一種以函數為主體的程式設計風格,這樣的技巧稱之為程序化程式設計(Procedural programming),也是我們至今以來習慣的寫作方式;然而另外有一種程式設計風格,是把即將要執行的程式碼組織為類別(Class),並視應用場景使用類別中的方法或資料來完成任務,這樣的技巧稱之為物件導向程式設計(Object-oriented programming, OOP)。具體而言,假如我們的程式碼所面對要解決的問題是非常明確且專一的,那麼使用程序化程式設計是很適當的,舉例來說,在購買刀具的時候,如果我們非常清楚是要用來削水果的,就會購買一把水果刀;反之如果程式碼所面對要解決的問題是比較廣泛且多元的,那麼使用物件導向程式設計就會更貼近,舉例來說,在購買刀具的時候只知道自己有使用刀具的需求,就會購買一個「刀具組」或者「萬能瑞士刀」,待需求發生時,再從其中取出對應的刀來使用。

常用的類別

在 Python 程式設計與資料分析的場景中,我們常用到的類別可以粗略分為三種:

  1. 內建的資料型別:整數、浮點數、字串、布林與 NoneType

  2. 內建的資料結構:串列、tuple、字典與集合。

  3. 第三方模組的資料結構:多維度陣列、IndexSeries 與資料框。

簡而言之,類別是一種能夠讓程式設計師自行定義資料型別或資料結構的機制,提供可以把資料與函數包裝起來的架構,並在定義完善以後使用,具體的使用方式就是我們在一開始學習程式設計時所學會的「宣告物件」,完整的說法是:物件(Object)是類別(Class)的實例(Instance),類別之於物件的關係就像是藍圖之於產品、設計圖之於成品或原稿之於印刷品一般。

# skywalker object is the instance of str
skywalker = "Anakin Skywalker" 
print(type(skywalker))
# skywalkers object is the instance of list
skywalkers = ["Anakin Skywalker", "Luke Skywalker"]
print(type(skywalkers))

程式設計師能夠使用類別包裝資料與函數,包裝好的資料稱為物件的屬性(Attributes),包裝好的函數稱為物件的方法(Methods)。我們可以用內建函數 dir() 來檢視有哪些屬性或方法。

skywalker = "Luke Skywalker"
skywalkers = ["Anakin Skywalker", "Luke Skywalker"]
print(dir(skywalker))
print(dir(skywalkers))

函數與方法的使用

我們原本提到的程式設計與資料分析核心精神:對資料應用函數,是偏向程序化程式設計的說法,在暸解類別觀念之後,同樣的核心精神可以修改為物件導向的說法:使用物件的方法

在 Python 中使用函數方式是在函數名稱後接上小括號,並在小括號中輸入物件名稱以及引數。

function_name(OBJECT, ARGUMENTS)

使用物件的方法是在物件名稱後接上句號(.)再接上方法名稱、小括號與引數。

OBJECT.method_name(ARGUMENTS)

例如 Python 有一個內建函數 sorted(),串列有一個方法 sort(),兩者都能夠排序一個串列,但是在使用的語法與意義上不同,使用內建函數 sorted() 排序串列是「對資料應用函數」的程序化程式設計說法,語法是 sorted(a_list_to_be_sorted);使用串列方法 sort() 排序是「使用物件的方法」的物件導向程式設計說法,語法是 a_list_to_be_sorted.sort()

primes_list = [11, 7, 5, 3, 2]
sorted(primes_list) # Apply sorted function to primes_list
print(primes_list)  # primes_list is not sorted
primes_list.sort()  # Call sort method of primes_list
print(primes_list)  # primes_list was sorted

值得注意的地方是,使用 sorted() 函數與串列的 sort() 方法分別表現了兩種物件更動的結果,sorted(primes_list) 以回傳值輸出排序後的串列,因此如果沒有將回傳值更新原本命名的串列,排序的更動並不會被保留。而 primes.sort() 則是直接將排序更動了,不需要更新原本命名的串列,也不會伴隨有回傳值。

自行定義類別

使用保留字 class 能夠自行定義類別,即便我們還沒有在類別設計任何資料或者函數(僅使用保留字 pass 讓自行定義類別的架構完整),但仍然可以生成一個「什麼事都做不了」的類別,值得注意的是自行定義的類別名稱我們習慣使用首字大寫的駝峰命名風格(Upper camel case)。

class ClassName:
    pass

例如自行定義一個簡單計算機類別 SimpleCalculator,在這個類別中只有一個說明文字。完成定義以後,我們宣告了物件 sc 作為 SimpleCalculator 類別的實例,並且利用內建函數 type()dir() 檢視類別名稱以及被包裝的資料跟函數。

class SimpleCalculator:
    """
    This class creates a simple calculator that is unable to do anything.
    """
    pass

sc = SimpleCalculator()
print(type(sc))
print(dir(sc))

從檢視的結果我們可以發現自行定義類別附屬於 __main__ 之中,同時物件 sc 也具有 __doc__ 以及其他許多前後都具有兩個底線的屬性或方法,前後都具有兩個底線的命名我們稱之為 Python 的特殊命名,比較單純的理解方式為 Python 為具有特殊用途的功能所保留的命名,為了避免和使用者的命名衝突,用前後都具有兩個底線的特殊格式區隔開,未來如果看到這樣的命名,就知道是 Python 預設的功能。這時我們可以將物件 sc__doc__ 屬性印出,會看到在自行定義類別中所寫下的說明文字。

print(sc.__doc__)

自行定義物件的方法

在自行定義類別的程式區塊中使用 def 保留字將函數與類別綁定,函數一但被歸屬在類別底下,成為實例之後就會改稱為物件的方法。

class ClassName:
    def method(self):
        # ...

這裡特別注意 self 參數,由於在自行定義類別的階段,就會設計類別實例化為物件之後的使用行為,因此在設計階段必須要給予物件一個代名詞,而 self 參數就是扮演物件的代名詞(事實上 self 參數可以任意地命名,但為了讓程式碼的可讀性增加,採用 self 做參數名稱是最佳的方式)。簡而言之,如果我們希望自行定義類別實例化為物件後可以這樣使用:

OBJECT.method(ARGUMENTS)

在自行定義類別的階段,就要加入 self 參數作為 OBJECT 的代名詞。例如自行定義的簡單計算機類別 SimpleCalculator,若是希望實例化物件 sc 可以這樣使用:

sc = SimpleCalculator()
sc.add(5, 6)

就必須要在設計階段納入 self 參數作為 sc 的代名詞。

class SimpleCalculator:
    def add(self, a, b):
        return a + b

sc = SimpleCalculator()
print(sc.add(5, 6))

自行定義物件的屬性

在自行定義類別的程式區塊中使用特殊函數 __init__() 將資料與類別綁定,資料一但被歸屬在類別底下,成為實例之後就會改稱為物件的屬性。

class ClassName:
    def __init__(self, attribute):
        # ...

這裡特別注意 __init__() 特殊函數,也常被稱為建構器(Constructor),這是在將類別實例化為物件的時候就會自動使用的函數,也因此我們可以在這個函數之下把資料都宣告妥當,由於還處於設計階段,同樣能透過 self 參數來作為物件的代名詞。簡而言之,如果我們希望自行定義類別實例化為物件後可以這樣使用:

OBJECT.attribute

在自行定義類別的階段,就要在 __init__() 函數下將資料與 self 綁定。例如自行定義的簡單計算機類別 SimpleCalculator,若是希望實例化物件 sc 可以這樣使用:

sc = SimpleCalculator(5, 6)
print(sc.a)
print(sc.b)

就必須要在 __init__() 函數下將參數宣告為 self 的資料。

class SimpleCalculator:
    def __init__(self, a, b):
        self.a = a
        self.b = b

sc = SimpleCalculator(5, 6)
print(sc.a)
print(sc.b)

在定義階段使用屬性或方法

若是在自行定義類別的階段中想要使用屬性,可以在自行定義類別的程式區塊中用 self.attribute 語法取用,例如將兩個屬性相加的 add() 方法可以這樣定義:

class SimpleCalculator:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def add(self):
        return self.a + self.b

sc = SimpleCalculator(5, 6)
print(sc.add())

若是在自行定義類別的階段中想要使用方法,可以在自行定義類別的程式區塊中用 self.method() 語法取用,例如將兩個屬性相加的 add() 方法能夠讓 add_and_square() 方法接續使用:

class SimpleCalculator:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def add(self):
        return self.a + self.b
    def add_and_square(self):
        return (self.add())**2

sc = SimpleCalculator(5, 6)
print(sc.add_and_square())

設計實例化後物件的顯示外觀

自行定義類別會具有一個名為 __repr__() 的特殊函數,它跟前面出現過的 __doc__ 屬性以及 __init__() 方法都屬於類別中預設作為某個功能的特殊命名,像是 __doc__ 屬性被保留給類別的說明文字,__init__() 方法被保留給類別實例化為物件時會自動使用的函數。而 __repr__() 函數的作用就是設計實例化後物件的顯示外觀,我們首先將前述沒有定義 __repr__() 的物件印出來檢視:

class SimpleCalculator:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def add(self):
        return self.a + self.b
    def add_and_square(self):
        return (self.add())**2

sc = SimpleCalculator(5, 6)
print(sc)

目前看到的是一個沒有註明該如何顯示物件外觀的狀態,因為在設計類別的過程中,並沒有在其中定義 __repr__() 函數。

接著將定義 __repr__() 函數加入到類別之中,再將有定義 __repr__() 函數後的類別實例化為物件印出來檢視:

class SimpleCalculator:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def __repr__(self):
        return "a: {}, b: {}".format(self.a, self.b)
    def add(self):
        return self.a + self.b
sc = SimpleCalculator(5, 6)
print(sc)

有無定義 __repr__() 函數差異一目瞭然。

封裝與繼承

前述我們透過自行定義類別能夠將資料與函數分別包裝成為類別的物件屬性(Attributes)以及類別的物件方法(Methods),這樣把函數實作細節包裝、隱藏起來的程式碼特性稱為封裝(Encapsulation)。

另外一種類別所具備的程式碼特性稱為繼承(Inheritance),這是用來讓先前定義好的類別得以延展功能的機制,透過繼承,新定義的類別可以完全沿襲所繼承類別的屬性與方法,這樣子可以有效解決程式碼的重複性問題,並且實踐軟體工程的 DRY(Don’t Repeat Yourself) 哲學。

只需要在新定義類別的名稱後在小括號內代入已定義完成的類別,即可像是「複製」一般把既有類別的所有功能在新定義類別中實現,由於這個機制使用了「繼承」這個動詞,所以常將已定義完成的類別稱為父母類別(ParentClass)、而將新定義類別稱為子女類別(ChildClass)。

class ChildClass(ParentClass):
    # ...

舉例來說,定義好的類別 SimpleCalculator 具有 __init__()、__repr__() 與 add() 這三個方法以及 a 與 b 兩個屬性;繼承了 SimpleCalculator 的 AnotherSimpleCalculator 類別在什麼事都不做的情況下(僅傳入一個 pass 保留字)就具備了三個方法以及兩個屬性:

class AnotherSimpleCalculator(SimpleCalculator):
    pass

another_sc = AnotherSimpleCalculator(5, 6)
print(another_sc)
print(another_sc.add())
print(another_sc.a)
print(another_sc.b)

如此一來在新定義的類別中添加方法,就可以引用繼承而來的方法以及屬性,達到功能延展並且不需要重新造輪的軟體工程 DRY(Don’t Repeat Yourself) 哲學。

在初步認識了類別之後,第十七週約維安計畫:Python 的函數來到尾聲,希望您也和我一樣期待下一篇文章。

延伸閱讀


對於這篇文章有什麼想法呢?喜歡😻、留言🙋‍♂️或者分享🙌

Leave a comment

計畫學員專區

付費訂閱電子報的會員可以點選 nbgitpuller 連結將本篇文章完整的 Jupyter Notebook 複製到自己的 Jupyter Server:

nbgitpuller 連結

伺服器選項選擇預設的「Data science environment:學習 Python、R 與 Julia 的資料科學環境」就可以順利運作文章中的範例程式。