認識物件導向

  1. 物件導向的優點是物件可以在不同的應用程式中被重複使用,Windows本身就是一個物件導向的例子。下面是幾個常見的名詞:

    • 物件(object) 或實體 (instance) 就像在生活中所看到的各種物體,在 Python 中,物件是資料與程式碼的組合,它可以是整個應用程式或應用程式的一部分。
    • 屬性 (attribute) 或成員變數 (member variable) 是用來描述物件的特質。
    • 方法 (method) 或成員方法 (member function) 是用來定義物件的動作。
    • 類別 (class) 是物件的分類,就像物件的藍圖或樣板,隸屬於相同類別的物件具有相同的屬性與方法,但屬性的值則不一定相同。

  2. 物件導向程式設計 (OOP,Object Oriented Programming) 主要有下列幾個特點:

    • 封裝 (encapsulation):物件導向程式設計將資料與用來處理資料的函式放在一起成為一個類別,稱為「封裝」,著重於物件與物件之間的操作。
    • 繼承 (inheritance):繼承指的是從既有的類別定義出新的類別,這個既有的類別叫做父類別 (parent class),而這個新的類別則叫做子類別 (child classsubclass)。
    • 多型 (polymorphism):多型指的是當不同的物件收到相同的訊息時,會以各自的方法來做處理。
    • Python 中的所有資料都是物件 (object),而物件的型別定義於類別 (class),類別就像物件的藍圖或樣板,裡面定義了物件的資料,以及用來操作物件的函式,前者稱為屬性 (attribute),後者稱為方法 (method)。
    • 物件是類別的實體 (instance),我們可以根據相同的類別建立多個物件,這個建立物件的動作稱為實體化 (instantiation)。
    • Python 中的物件都有編號 (id)、型別 (type) 與 (value),可以透過下列函式取得這些資訊:
      • id(x)
      • type(x)
      • print(x)

類別與物件

較完整的應用程式通常由許多類別組成,Python 其實是一種物件導向(Object Oriented Programming) 程式語言,可以建立類別後再根據類別建立物件。

  1. 建立類別

    • 建立類別
      class 可以建立類別,類別名稱的第一個字元建議使用大寫字元。語法:

      class 類別名稱():

      例如: 建立類別 Animal,其中 「()」也可以省略,寫成 「class Animal:」

      class Animal():
    • 類別的屬性方法
      類別中通常會建立屬性 (attribute) 和方法 (method),提供物件使用,類別中的屬性其實就是一般的變數,而方法則是函式,但在類別中不以變數和函式稱呼,而是稱為屬性和方法。定義的方法中第一個參數必須是 self,第二個以後的參數則可依實際需要增加或省略。

      以類別名稱即可建立物件 (object)。語法:

      物件=類別()

      然後以物件執行其屬性和方法。

      物件.屬性
      物件.方法()

    【案例】
    建立類別Animal,並在類別建立name 屬性和sing方法。

  2. 類別的建構式(Constructor)
    當我們透過類別實例化(建立)一個物件時,Python 會自動呼叫一個特殊方法來初始化這個物件,這個方法就是 __init__(),也就是所謂的建構式,建立建構式的語法:

    def __init__(self[, 參數1, 參數2, ...]):
         # 建立物件屬性
         self.屬性名稱 = 參數
    • self:代表「正在被建立的這個物件本身」,必須放在第一個參數的位置。
    • 預設建構式:若定義類別時未寫 __init__(),Python 會自動提供一個不帶自訂參數的預設建構式(即 def __init__(self): pass)。
  3. 類別的解構式(Destructor)

    相對建構式是在物件「誕生」時執行,解構式(__del__ 則是在物件「消逝/被銷毀」時自動執行的特殊方法。

    當一個物件不再被使用(例如程式執行完畢、或者手動使用 del 刪除該物件)時,Python 的垃圾回收機制(Garbage Collector)會清理該物件所占用的記憶體空間,並在清理前自動呼叫 __del__(),建立解構式的語法:

    def __del__(self):
        # 清理資源的程式碼(如關閉檔案、斷開資料庫連線等)

【案例】

【預期輸出結果】

【誕生】鸚鵡 被建立好了!
鸚鵡
鸚鵡,很會唱歌!
準備執行 del bird ...
【銷毀】鸚鵡 離開了,記憶體空間已釋放。
程式執行完畢。
  1. 屬性初始值的設定

    class01.py3「name="小鳥"」可以設定name的初始值,但無法在建立物件時就直接初始化,如果將初始化的動作放在__init__()建構式中,這樣我們就可以在建立物件時,透過參數設定其初始值。

【案例】
建立物件bird預設屬性name="鸚鵡"age=1

程式說明

⬜ 2 def __init__(self, name, age): 接收兩個參數 nameage,以 self.nameself.age 設定物件屬性。
⬜ 5 def sing(self): 印出名稱、年齡與訊息。
⬜ 7 def grow(self, year): 讓年齡增加 year 歲。
⬜ 9 bird = Animal("鸚鵡", 1):以 Animal 類別建立一個名叫鸚鵡、1 歲大的 bird 物件。
⬜ 10 bird.grow(1):讓 bird 長大 1 歲,年齡變為 2。
⬜ 11 bird.sing():印出 鸚鵡2歲,很會唱歌!

建構式與解構式比較表

特性建構式 (init)解構式 (del)
執行時機建立物件時自動執行物件被銷毀/記憶體釋放時自動執行
主要用途設定屬性初始值、分配資源釋放外部資源(如關閉檔案、資料庫連線)
預設行為未定義時由 Python 提供無動作預設值未定義時由 Python 提供無動作預設值
常態呼叫bird = Animal("鸚鵡")del bird 或程式結束時自動呼叫
  1. 匿名物件
    我們可以用一個很貼切的生活比喻來理解匿名物件(Anonymous Object)

    • 具名物件(有變數名稱):像是在動物園裡養一隻鸚鵡,給牠取名叫 bird(指派給變數)。你可以今天餵牠 bird.grow(1),明天叫牠唱歌 bird.sing(),因為你有一個固定名稱(變數)持續關心與追蹤這隻特定的鸚鵡。
    • 匿名物件(沒有變數名稱):像是你在路上臨時遇到一隻野生鸚鵡,你當下對牠做了一件事(比如聽牠唱一句歌),做完之後你沒有把牠帶回家命名,牠就飛走了。下一次你再呼叫同樣的類別,Python 會建立一隻全新的物件,跟前一隻毫無關係。

    【案例】

    因為沒有用變數(如 bird = ...)把第一隻鸚鵡「綁住/存下來」,所以執行完 第13grow(1) 後,第一隻鸚鵡就消失在記憶體中了;第14行呼叫 sing() 時,Python 是重新現場打造了一隻全新的 1 歲鸚鵡。

    什麼時候才需要用「匿名物件」?
    匿名物件通常用在「一次性使用、用完即丟」的情境。你不需要特地花一個變數名稱去記住它,只需要用它一次性的功能或結果。


類別封裝

class03.py 程式中,可以 bird.age 存取 age 屬性,因此就可以設定 bird.age=-1 設定 bird 年齡為 -1 歲,這樣直接從外部設定 年齢<0 的方式其實並不合理,因此必須對 age 屬性作適度的保護。
類別中可以讓外部引用的屬性稱為共用 (public) 屬性、方法稱為共用方法,在 class03.py 程式中,年齡 age 應該以 bird.grow() 方法增加,不可以從外部以 bird.age 直接設定。
Python 提供私用 (private) 屬性和私用方法,這種私用屬性私用方法只有類別內內部可以使用,類別外部並無法使用,這樣的觀念稱為 封裝(encapsulation)
在屬性和方法前面加上 __ (兩個 __ 字元),就成為 私用屬性方法。例如: 類別中建立 __name__age 屬性和 __sing方法。

【案例】

類別外部並無法使用私用屬性方法,因此如果在第 14列行 bird.__age =-1 無效,第16列執行bird.__sing()將會產生錯誤。


類別繼承

類別可以繼承,被繼承的類別稱為父類別 (parent class) 或基底類別(base class),繼承的類別稱為子類別 (child class)或衍生類別 (derived class),子類別可以繼承父
類別中所有共用屬性和共用方法。在程式設計時,請注意父類別必須放在子類別的前面。

  1. 建立子類別
    建立子類別的語法:

    class 類別名稱(父類別):

    例如: 建立類別 Bird 繼承 Animal 類別,其中 Animal是父類別,Bird 是子類別。

    class Bird(Animal):

    子類別會繼承父類別的所有共用屬性和方法,也可以建立屬於自己的屬性和方法。

    例如: 建立父類別 Animal,包含 namefly 共用屬性和方法,再建立子類別 Bird 繼承 Animal 類別,並在Bird 類別中建立另一個共用方法 sing

    【案例】
    請設計一個簡單的動物類別繼承結構,包含父類別 Animal 與子類別 Bird

    1. 建立父類別 Animal
      • 建構式:接收一個參數 name,並將其設定為物件屬性 self.name
      • 方法 fly():呼叫時會印出 "{name}很會飛!"(例如:小白鴿很會飛!)。
    2. 建立子類別 Bird(繼承自 Animal
      • 覆寫建構式 __init__():接收參數 name,但在設定屬性時,必須將其修改覆寫為 "粉紅色" + name
      • 新增方法 sing():呼叫時會印出 "{name}也愛唱歌!"(例如:粉紅色小鸚鵡也愛唱歌!)。
    3. 實例化與測試
      • 使用 Animal 類別建立一個名為 "小白鴿" 的物件 pigeon,並呼叫其 fly() 方法。
      • 使用 Bird 類別建立一個名為 "小鸚鵡" 的物件 parrot,並分別呼叫其 fly()(繼承自父類別)與 sing()(子類別自訂)方法。

    【預期輸出結果】

    小白鴿很會飛!
    粉紅色小鸚鵡很會飛!
    粉紅色小鸚鵡也愛唱歌!

    程式說明
    ⬜ 7 Bird 繼承 Animal 類別,也繼承了 namefly共用屬性和方法。
    ⬜ 9 覆寫父類別的建構式。
    ⬜ 10-11 建立專屬於 Bird 子類別的方法 sing
    ⬜ 13 以 Animal類別,建立一個名叫小白鴿的 pigeon 物件。
    ⬜ 14 執行 Animal 父類別的fly 方法。
    ⬜ 16 以 Bird 類別,建立一個名叫小鸚鵡的 parrot 物件。
    ⬜ 17 執行繼承 Animal 父類別的 fly方法。
    ⬜ 18 執行 Bird 子類別的 sing 方法。

  2. 子類別和父類別擁有相同的屬性和方法
    有的時侯會碰到子類別和父類別擁有相同的屬性和方法,此時子類別會先尋找子類別中是否有此名稱的屬性和方法,如果有找到就使用子類別的的屬性和方法,否則就使用父類別的的屬性和方法。

    子類別也可用 super() 方法執行父類別的方法。
    【案例】
    建立類別 Bird 繼承類別 Animal,子類別 Bird 再以 super() 方法覆寫 __init__()fly() 方法。

    【預期輸出結果】

    小白鴿很會飛!
    2歲小鸚鵡很會飛!

    程式說明
    ⬜ 8-10 def __init__(self,name,age): 接收兩個參數,其中age為年齡,super().__init__(name) 執行父類別的__init__()方法。
    ⬜ 11-13 def fly(self):super().fly()執行父類別的 fly方法。
    ⬜ 15-16 執行的是 Animal 父類別的 fly方法。
    ⬜ 18-19 執行的是 Bird子 類別的的 fly方法。

    在 Python 程式碼中,if __name__ == "__main__": 是一個非常經典且重要的結構。它的主要用途是:判斷當前這個 Python 檔案是被「直接執行」還是被「當成模組(module)匯入(import)」

    1. __name__ 是什麼?
      __name__ 是 Python 內建的一個特殊變數(系統變數):

      • 當直接執行該檔案時:Python 會自動將這個檔案的 __name__ 變數賦值為 "__main__"
      • 當該檔案被其他檔案 import__name__ 變數的值會變成「該檔案的檔名」(不含.py 副檔名)。
    2. 用途:防止「匯入時自動執行測試程式碼」
      以你的程式為例,假設這個檔案叫做 class06.py

       class Animal:
           ...
       class Bird(Animal):
           ...
           
       if __name__ == "__main__": 
            pigeon = Animal("小白鴿") 
            pigeon.fly() ...
      • 情況 A(直接執行 class06.py__name__ 等於 "__main__",條件成立,縮排裡面的測試程式碼(建立 pigeonparrot 並印出訊息)會正常執行

      • 情況 B(在另一個檔案 main.py 中匯入它)

        # main.py
        import class06 # 匯入類別
         
        my_bird = class06.Bird("文鳥", 1)

        main.py執行import class06時,Python 會把 class06.py 從頭到尾讀取一遍以載入類別定義。
        此時在class06.py內部,__name__的值是"class06"(不是 "__main__"),因此 if 條件不成立,底下的測試程式碼完全不會被執行

    3. 何時必須/建議加入這一行?

      情況是否建議加入?原因與說明
      撰寫可重用的模組/函式庫必須加入當你寫的.py檔案主要是定義 Class 或 Function,打算給其他專案import時,將範例或測試程式碼放在這段判斷式內。
      單一獨立執行的主程式入口建議加入作為整個專案的「程式執行入口」(Main Entry Point),讓程式結構更清晰、規範。
      單一檔案的腳本(Script)或作業可以不加如果這個 .py 檔只是自己交作業或一次性腳本,絕對不會被其他檔案 import,不加程式也能正常運作,但加上是養成良好的寫作習慣。
  3. 鏈狀繼承(Chained Inheritance)

    • 結構單向縱向延伸(一對一的直線鏈條,像「祖孫關係」)。
    • 特徵:一個子類別只有一個直接父類別,但透過一層層向上繼承,擁有整條鏈條上所有祖先類別的特徵。
    • 關係圖

    【案例】

    【說明】

    • obj 是類別 C 的實例。
    • obj.z:在 C 類別本身找到 z = 3
    • obj.yC 本身沒有 y,順著繼承鏈向上至父類別 B 找到 y = 2
    • obj.xCB 都沒有 x,繼續向上至祖父類別 A 找到 x = 1
  4. 多重繼承(Multiple Inheritance)

    • 橫向整合:與「鏈狀繼承(祖孫關係)」不同,多重繼承就像是孩子同時繼承了父親與母親的基因與技能。

    • 技能組合:子類別可以同時擁有來自 父類別1父類別2父類別n 的所有屬性與方法,讓子類別具備複合式的功能。

    Python 支援多重繼承,一個子類別可以繼承多個父類別,語法:

    class 子類別名稱(父類別1,父類別2,...,父類別n)

    程式說明
    ⬜ 10 Child 同時繼承了 FatherMother,因此擁有 drive()cook() 兩個方法。
    ⬜ 14 kid.drive() 執行的是繼承自 Father 類別的 drive 方法。
    ⬜ 15 kid.cook() 執行的是繼承自 Mother 類別的 cook 方法。
    當子類別與多個父類別擁有相同名稱的方法時,Python 會依照 MRO(Method Resolution Order)順序尋找,即先找 Child,再依繼承順序找 Father,最後找 Mother


多型

前面不同類別中擁有相同的方法名稱,這樣的觀念稱為多型(polymorphism),但其實多型不一定要有繼承關係,它的好處是同一個方法名稱卻可以產生不同的功能。

【案例】
定義 Bird 繼承Animal類別,再定義另一個 Plane類別,這3個類別都擁有fly方法。

程式說明
⬜ 16-17 執行的是Animal 類別的 fly 方法。
⬜ 19-20 執行的是Bird 類別的 fly方法。
⬜ 22-24 執行的是 Plane 類別的flyfly_mile方法。

取得父類別的私用屬性
基於對私用屬性的保護,類別之外並無法取得類別內的私用屬性,包括它的子類別也無法讀取,如果一定非取得不可,就只能以「return 私用屬性」的方式,將私用屬性傳回。 例如: 在子類別中以super().getEye()取得父類別的私用屬性「self.__eye」


過載(Overloading)

在物件導向程式設計(OOP)中,「多型(Polymorphism)」「過載(Overloading)」 是兩個常常被放在一起討論,但層次不同的概念:

核心結論: 過載(Overloading) 是一種實現 多型(Polymorphism) 的「手段或形式」之一。

  • 多型觀念:指的是「同一個介面/方法名稱,可以根據情境呈現不同的行為或處理不同的資料」。

  • 過載實現方式:指的是「在同一個類別中,定義多個名稱相同參數數量或型態不同的方法」。

但在 Python 中,這兩者的關係非常特殊,主要原因如下:

1. 傳統語言中的關係(如 C++ / Java)

在傳統靜態型別語言中,多型主要分為兩種型態:

  1. 編譯期多型(Compile-time Polymorphism)透過「過載 (Overloading)」實現
    • 編譯器在執行前,會根據你傳入的參數數量與型態,自動決定呼叫哪一個同名方法。
  2. 執行期多型(Runtime Polymorphism)透過「覆寫 (Overriding / 繼承)」實現
    • 子類別重寫父類別的方法,物件在執行時根據自己的實際型態展現不同行為。

2. Python 中「過載」與「多型」的特殊關係

Python 是一門動態型別語言(Dynamic Typing),且遵循鴨子型態(Duck Typing)哲學:「如果它走起來像鴨子,叫起來像鴨子,那它就是鴨子」。

這導致 Python 對這兩者的處理方式與其他語言不同:

① Python 原生不支援「語法上的方法過載」

如上一題所提到的,如果在 Python 的同一個 class 中寫了兩個同名方法,後者會直接蓋掉前者。因此,Python 無法像 Java 那樣直接透過寫多個同名函式來實現「過載多型」。

② Python 用「極度彈性的參數」取代了過載

雖然 Python 沒有傳統的過載語法,但它透過以下機制,用單一方法就達成了過載的效果

  • 預設參數(Default Arguments)def add(x, y=0)
  • 不定長度參數(*args,**kwargs)
  • 型態檢查與動態判斷

這意味著:在 Python 中,你不需要寫多個過載方法來達成多型,寫一個彈性極高的方法即可。

1. 使用 Default Arguments

【案例】
使用預設參數(Default Arguments),透過檢查傳入的參數數量或使用 isinstance() 判斷型態,在同一個方法內部寫邏輯判斷。

【預期輸出結果】

單一數字處理結果:15
數字相加結果:30
字串串接結果:Hello - World
2. 使用 *args

*args 讓函式可以接受任意數量的位置參數(positional arguments),不需要事先固定參數個數。

【案例】
使用*args,透過檢查傳入的參數數量或使用 isinstance() 判斷型態,在同一個方法內部寫邏輯判斷。

【預期輸出結果】

單一數字處理結果:15
數字相加結果:30
字串串接結果:Hello - World
3. 使用內建裝飾器 @singledispatchmethod(依型態分派)

如果你希望程式碼看起來更像傳統語言的多型,可以使用 Python 內建 functools 模組提供的裝飾器,來達到「依據傳入參數的資料型態自動找對應方法」的效果:

【案例】
使用 @singledispatchmethod 裝飾器時,主要是依據第一個參數的資料型態來決定呼叫哪一個方法。

因為傳入兩個字串和傳入數字時的行為不同,我們可以針對 int(數字)與 str(字串)分別註冊不同的處理函式:

【預期輸出結果】

單一數字處理結果:15
數字相加結果:30
字串串接結果:Hello - World

程式說明

  1. @singledispatchmethod
    • 放在主要的 add 方法上方,將其設定為「分派器(Dispatcher)」。
    • 當傳入的第一個參數 x 是整數(例如 510)時,會直接執行這個預設方法。
  2. @add.register
    • 透過在註冊語法中標註型態提示 x: str,告誡 Python:「當第一個參數是字串時,改執行這個專屬的 function」
    • 方法名稱使用底線 _ 即可,因為呼叫時一律使用 calc.add(...)

③ Python 的多型核心在於「鴨子型態」與「覆寫(Overriding)」

Python 的多型不需要依賴過載,而是依賴物件是否具備同名的方法

只要不同的物件擁有相同名稱的方法,Python 就能直接呼叫,完全不需要關心物件的類別是否相同或是否有繼承關係:

觀念對比總結表

比較維度多型(Polymorphism)過載(Overloading)
定義性質物件導向的核心觀念與特性實現多型的其中一種語法手段
主要目的讓不同的物件能響應相同的呼叫讓同一個名稱的方法能接受不同的參數組合
Python 的支援度完全支援(主要靠鴨子型態與方法覆寫)原生不支援(需改用 *args、預設參數或裝飾器模擬)
主要實現場合不同的類別間(或繼承結構中)同一個類別內部

介面 Interface

Python 本身沒有像 JavaC# 那種正式的 interface 關鍵字
但! Python 仍然可以用好幾種方式達成 「介面」 的效果:

方法: 使用 abc 模組(最正式、最接近 interface)

Python

from abc import ABC, abstractmethod

可以創建「抽象類別」(abstract class),等同於介面。

範例:

  • 定義介面

    from abc import ABC, abstractmethod
    class Animal(ABC):  # 介面(抽象類別)
        @abstractmethod
        def sound(self):
            pass
  • 實作介面

    class Dog(Animal):
        def sound(self):
            return "汪汪"
    class Cat(Animal):
        def sound(self):
            return "喵喵"
  • 使用

    d = Dog()
    print(d.sound())

類別應用

範例: 計算面積

定義RectangleTriangle 兩個類別,父類別 Rectangle定義共用屬性 widthheight
area()方法計算矩形面積。Triangle 子類別繼承 Rectangle 類別並增加一個計算三角形面積的方法area2()

程式說明
⬜ 1-6 建立父類別 Rectangle
⬜ 2-4 建立父類別建構式。
⬜ 5-6 建立 area 方法計算矩形面積。
⬜ 8-10 建立子類別 Triangle 繼承 Rectangle 類別。
⬜ 9-10 建立 area2 方法計算三角形面積。
⬜ 12 Triangle(5,6)建立子類別的物件triangle,並初始化。
⬜ 13-14 計算矩形面積、三角形面積。