Skip to content

Python OOP:Class、Object、繼承

當你開始學習 Python 編程時,最初可能習慣使用函數 (functions) 來組織你的程式碼。將重複的任務或邏輯區塊封裝成函數,可以讓程式碼更清晰、更易於管理。這種主要以函數為組織單位的編程方式,通常被稱為程序導向編程 (Procedural Programming)。它強調的是執行的步驟和程序。

然而,隨著專案變得越來越複雜,你可能會發現單純使用函數來組織所有東西開始變得困難。數據和操作這些數據的函數散落在各處,不易維護。這時,物件導向編程 (Object-Oriented Programming, OOP) 就提供了一種更進階、更現代的解決方案。

OOP 是一種範式轉移 (paradigm shift)。它不再僅僅是組織「動作」(函數),而是將「數據」(data) 和操作這些數據的「相關函數」(現在稱為「方法」methods) 一起打包到稱為「類別 (Class)」的結構中。每一個根據類別創建出來的具體數據實例,就是一個「物件 (Object)」。學習 OOP 對於編寫更大型、更模組化、更易於維護和擴展的 Python 應用程式至關重要。

這篇文章會帶你一步步了解 Python 中的 OOP,從類別和物件的基礎,到物件的身份,再到 OOP 的一個重要支柱——繼承 (Inheritance)。

🤔 什麼是物件導向編程 (OOP)?

物件導向編程 (Object-Oriented Programming, OOP) 是一種程式設計的思維模式 (paradigm)。它的核心思想是將程式碼組織成一系列相互作用的「物件」。

相較於程序導向編程專注於「執行什麼操作」,OOP 更關注:

  1. 將數據 (Attributes) 和操作該數據的方法 (Methods) 綁定在一起:這就是「類別」和「物件」的核心概念——封裝 (Encapsulation)。
  2. 模擬現實世界的實體或概念:例如,一個人、一輛車、一個學生,都可以被視為物件,每個物件都有其狀態 (數據) 和行為 (方法)。

簡單來說,OOP 提供了一種更有條理、更模組化的方式來設計和編寫程式。它使得程式碼更易於理解、重用和擴展。

🧱 類別 (Class) 和物件 (Object):藍圖與實例

理解 OOP 的關鍵在於掌握「類別 (Class)」和「物件 (Object)」這兩個核心概念。

類別 (Class):物件的藍圖 📜

一個類別 (Class) 就像是一個設計藍圖、一個模板或者一個食譜。它定義了一種物件的共同特徵 (屬性) 和行為 (方法)。

  • 屬性 (Attributes):物件擁有的數據或狀態。例如,一個 Person 類別可能有 name(姓名)和 age(年齡)這些屬性。
  • 方法 (Methods):物件可以執行的動作或功能。這些是在類別中定義的函數。例如,一個 Person 類別可能有 greet()(打招呼)這個方法。

類別本身並不佔用記憶體來存儲具體的數據值,它只是一個定義。

物件 (Object):類別的具體實例 🏠

一個物件 (Object) 是根據類別這個藍圖創建出來的具體實例 (instance)。每一個物件都擁有類別所定義的屬性,並且可以執行類別所定義的方法。

  • 如果 Person 是一個類別(藍圖),那麼「Paul,30歲」就是一個 Person 物件,「Mary,25歲」是另一個 Person 物件。
  • 每個物件都有自己獨立的屬性值。Paul 的 name 是 "Paul",Mary 的 name 是 "Mary"。
  • 所有 Person 物件共享 greet() 方法的定義,但當方法被調用時,它會作用於調用它的那個特定物件的數據。

重點:每一個物件,就是一個「數據包」。它把描述這個實體所需的數據(屬性)和操作這些數據的函數(方法)封裝在一起。每個物件在 Python 中都有一個身份 (identity)。

🛠️ Python 如何定義類別和創建物件

現在,來看看如何在 Python 中實際操作。

定義一個類別 (Class)

使用 class 關鍵字來定義一個類別。按照慣例,類別名稱通常以大寫字母開頭 (PascalCase)。

python
class Person:
    # 這是 __init__ 方法,也稱為「建構子」(constructor) 或「初始化方法」
    # 當你創建一個 Person 物件時,這個方法會自動被調用
    def __init__(self, name, age):
        # "self" 代表物件的實例本身
        # "self.name" 和 "self.age" 是物件的屬性 (attributes)
        self.name = name
        self.age = age
        print(f"一個名為 {self.name} 的 Person 物件 (ID: {id(self)}) 被創建了!")

    # 這是物件的方法 (method)
    def greet(self):
        print(f"你好,我叫 {self.name},今年 {self.age} 歲。(來自物件 ID: {id(self)})")

    def celebrate_birthday(self):
        self.age += 1
        print(f"Happy Birthday, {self.name}! 你現在 {self.age} 歲了。")

解說:

  • class Person::定義了一個名為 Person 的新類別。
  • __init__(self, name, age)
    • 這是一個特殊的方法,稱為初始化方法 (initializer) 或建構子 (constructor)。
    • 當你創建一個新的 Person 物件時,Python 會自動調用 __init__ 方法。
    • self 參數是必須的,它代表正在被創建或操作的物件實例。你可以把它想像成「這個物件自己」。
    • nameage 是傳遞給建構子的參數,用於初始化物件的屬性。
    • self.name = nameself.age = age 創建了物件的屬性,並將傳入的值賦給它們。這裡的 self.nameself.age 就是這個物件所擁有的數據。
  • greet(self)celebrate_birthday(self)
    • 這些是 Person 類別的方法。
    • 它們像普通函數一樣定義,但第一個參數必須是 self
    • 方法可以訪問和修改物件的屬性(例如 self.nameself.age)。

創建物件 (Instantiation)

定義了類別之後,就可以用它來創建物件了。這個過程稱為「實例化」(Instantiation)。

python
# 創建第一個 Person 物件 (一個數據實例)
person1 = Person("Paul", 30)
# 輸出: 一個名為 Paul 的 Person 物件 (ID: <some_id_1>) 被創建了!

# 創建第二個 Person 物件 (另一個數據實例)
person2 = Person("Mary", 25)
# 輸出: 一個名為 Mary 的 Person 物件 (ID: <some_id_2>) 被創建了!

# person1 和 person2 是 Person 類別的兩個不同物件
# 它們各自擁有獨立的 name 和 age 數據,以及不同的物件 ID
print(f"person1 的 ID: {id(person1)}")
print(f"person2 的 ID: {id(person2)}")
# 可以看到 person1 和 person2 的 ID 是不同的

# 訪問物件的屬性
print(f"{person1.name} 的年齡是: {person1.age}") # 輸出: Paul 的年齡是: 30
print(f"{person2.name} 的年齡是: {person2.age}") # 輸出: Mary 的年齡是: 25

# 調用物件的方法
person1.greet()  # 輸出: 你好,我叫 Paul,今年 30 歲。(來自物件 ID: <some_id_1>)
person2.greet()  # 輸出: 你好,我叫 Mary,今年 25 歲。(來自物件 ID: <some_id_2>)

person1.celebrate_birthday() # 輸出: Happy Birthday, Paul! 你現在 31 歲了。
print(f"{person1.name} 的新年齡是: {person1.age}") # 輸出: Paul 的新年齡是: 31

在上面的例子中,person1 是一個物件,它封裝了關於「Paul」這個人的數據(姓名和年齡)以及可以對這些數據進行的操作(greetcelebrate_birthday)。同樣,person2 是另一個獨立的物件,代表「Mary」。每一個物件都是一個獨立的數據實體,並且擁有獨一無二的身份 (identity),你可以使用 id() 函數來查看。

🔗 繼承 (Inheritance):建立類別之間的層次關係

OOP 的一個強大特性是繼承 (Inheritance)。繼承允許你創建一個新的類別(稱為子類別 Subclass衍生類別 Derived Class),這個新類別可以繼承一個已存在的類別(稱為父類別 Parent Class超類別 Superclass基底類別 Base Class)的屬性和方法。

這有助於:

  1. 程式碼重用:子類別自動獲得父類別的特性,無需重複編寫。
  2. 建立「is-a」關係:例如,一個 Student is a PersonStudent 擁有 Person 的所有基本特徵,同時也可能有自己獨特的特徵。
  3. 階層式結構:可以建立更複雜和有組織的類別結構。

繼承範例:PersonStudent

讓我們擴展之前的 Person 類別,並創建一個 Student 子類別。

python
class Person:  # 父類別 / 基底類別
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"一個 Person 物件 {self.name} (ID: {id(self)}) 被創建。")

    def greet(self):
        print(f"你好,我叫 {self.name},今年 {self.age} 歲。")

    def celebrate_birthday(self):
        self.age += 1
        print(f"Happy Birthday, {self.name}! 你現在 {self.age} 歲了。")

# Student 類別繼承自 Person 類別
class Student(Person):  # 子類別 / 衍生類別
    def __init__(self, name, age, student_id):
        # 調用父類別 Person 的 __init__ 方法來初始化 name 和 age
        super().__init__(name, age)
        # Student 特有的屬性
        self.student_id = student_id
        self.courses = []
        print(f"一個 Student 物件 {self.name} (ID: {id(self)}),學號 {self.student_id},被創建。")

    # Student 特有的方法
    def enroll_course(self, course_name):
        self.courses.append(course_name)
        print(f"學生 {self.name} (學號: {self.student_id}) 已成功報讀課程: {course_name}")

    def list_courses(self):
        if self.courses:
            print(f"學生 {self.name} 修讀的課程有: {', '.join(self.courses)}")
        else:
            print(f"學生 {self.name} 目前沒有修讀任何課程。")

    # 方法覆寫 (Method Overriding): Student 可以有自己的 greet 版本
    def greet(self):
        # 可以選擇調用父類別的 greet 方法
        # super().greet()
        print(f"大家好,我是學生 {self.name},學號是 {self.student_id},今年 {self.age} 歲。")

# 創建 Person 物件
person_generic = Person("David", 40)
person_generic.greet()

print("-" * 20)

# 創建 Student 物件
student1 = Student("John", 19, "S2025001")
student1.greet()  # 調用 Student 覆寫後的 greet 方法
student1.celebrate_birthday() # 繼承自 Person 的方法
student1.enroll_course("Python程式設計")
student1.list_courses()

print("-" * 20)

student2 = Student("Alice", 20, "S2025002")
student2.greet()
student2.enroll_course("數據分析")
student2.list_courses()

# 檢查繼承關係
print(f"student1 是 Person 的一個實例嗎? {isinstance(student1, Person)}")   # True
print(f"student1 是 Student 的一個實例嗎? {isinstance(student1, Student)}") # True
print(f"person_generic 是 Student 的一個實例嗎? {isinstance(person_generic, Student)}") # False
print(f"Student 類別是 Person 類別的子類別嗎? {issubclass(Student, Person)}") # True

解說:

  • class Student(Person)::這表示 Student 類別繼承自 Person 類別。
  • super().__init__(name, age):在 Student__init__ 方法中,super() 函數被用來調用其父類別 (Person) 的 __init__ 方法。這確保了從 Person 繼承的屬性 (name, age) 能夠被正確初始化。
  • self.student_idself.courses:這些是 Student 類別特有的屬性。
  • enroll_course()list_courses():這些是 Student 類別特有的方法。
  • 方法覆寫 (Method Overriding)Student 類別重新定義了 greet() 方法。當 student1.greet() 被調用時,執行的是 Student 類別中定義的版本,而不是 Person 類別中的版本。如果子類別沒有覆寫父類別的方法,則會使用父類別的方法 (如 celebrate_birthday())。
  • isinstance()issubclass():這些內建函數可用於檢查物件的類型和類別之間的繼承關係。

繼承是 OOP 中實現程式碼重用和建立清晰類別層次結構的強大工具。

✨ Python 中萬物皆物件,且每個物件皆有身份 (Identity)

一個非常重要且強大的 Python 特性是:在 Python 中,萬物皆物件 (object)。

這意味著你日常使用的所有東西,比如數字、字串、列表、字典,甚至函數和類別本身,它們都是某個類別的物件。

更進一步,每個物件在記憶體中都有一個獨一無二的身份標識 (identity)。你可以使用內建的 id() 函數來獲取這個身份標識,它通常代表物件在記憶體中的地址 (雖然具體實現可能因 Python 版本和平台而異,但你可以視它為一個唯一識別碼)。這個身份在物件的生命週期內保持不變。

python
# 數字是物件
num1 = 100
num2 = 100
num3 = 200
print(f"num1: value={num1}, type={type(num1)}, id={id(num1)}")
print(f"num2: value={num2}, type={type(num2)}, id={id(num2)}") # Python 可能會對小整數進行快取,所以 id 可能相同
print(f"num3: value={num3}, type={type(num3)}, id={id(num3)}")

# 字串是物件
text1 = "Hello"
text2 = "Hello"
print(f"text1: value='{text1}', type={type(text1)}, id={id(text1)}")
print(f"text2: value='{text2}', type={type(text2)}, id={id(text2)}") # 字串也可能因為 intern 機制而有相同 id
print(text1.upper()) # str 物件有 upper() 方法

# 列表是物件
list1 = [1, 2, 3]
list2 = [1, 2, 3] # 即使內容相同,也是不同的列表物件
list3 = list1     # list3 和 list1 指向同一個列表物件
print(f"list1: id={id(list1)}, list2: id={id(list2)}, list3: id={id(list3)}") # list1 和 list3 的 id 相同

# 函數也是物件
def my_function():
    print("This is a function.")
print(f"my_function: type={type(my_function)}, id={id(my_function)}")

# 類別本身也是物件 (type 類別的實例)
print(f"Person class: type={type(Person)}, id={id(Person)}")
print(f"Student class: type={type(Student)}, id={id(Student)}")

理解「萬物皆物件」以及「每個物件皆有身份」這兩個概念,有助於你更深入地了解 Python 的運作方式。id() 函數可以幫助你驗證變數是否指向同一個記憶體中的物件,這在處理可變物件 (mutable objects) 如列表時尤其重要。

💡 OOP:一種更進階的程式碼組織方式

回到最初的觀點:從程序導向(使用函數)到物件導向(使用類別和物件)是一種進步。

  • 函數 (Functions):將執行特定任務的程式碼塊組織起來。這是程序導向編程的核心。
  • 類別與物件 (Classes & Objects):將組織提升到一個新的層次。它們允許你將數據 (屬性) 和操作這些數據的函數 (方法) 緊密地捆綁在一起 (封裝),形成一個個獨立的、有意義的單元 (物件)。
  • 繼承 (Inheritance):允許類別之間共享特性,並建立層次關係,促進程式碼重用和擴展性。

這種將數據和方法捆綁在一個單元(物件)中的做法,是 OOP 的一個核心原則,稱為封裝 (Encapsulation)。結合繼承和其他 OOP 原則(如多型 Polymorphism 和抽象 Abstraction,本文未詳細介紹),OOP 提供了一套強大的工具來構建複雜的軟體系統。

🎯 總結

物件導向編程 (OOP) 是一種強大的程式設計典範,它透過類別 (Class)、物件 (Object) 和繼承 (Inheritance) 等核心概念,幫助我們更有效地組織和管理程式碼。

核心要點回顧:

  • 程序導向編程主要使用函數組織程式碼。
  • OOP 是一種典範轉移,將數據和相關方法封裝在類別中,是更現代的程式碼組織方式。
  • 類別 (Class) 是創建物件的藍圖。
  • 物件 (Object) 是類別的具體實例,每個物件都是一個獨立的數據包,擁有自己的數據和方法。
  • 在 Python 中,萬物皆物件,每個物件都有一個獨一無二的身份 (identity),可使用 id() 函數查看。
  • 繼承 (Inheritance) 允許子類別繼承父類別的屬性和方法,實現程式碼重用和建立類別層次。
  • 學習 OOP 對於編寫更模組化、可重用和可維護的 Python 程式至關重要。

雖然 OOP 還有更多進階概念,但理解類別、物件、物件身份和繼承是踏入 OOP 世界的關鍵步驟。希望這篇文章能幫助你對 Python 中的 OOP 有一個清晰的認識!

📚 參考資料

KF Software House