iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0

前言

昨天我們學會了:

  • 類別(Class):像藍圖
  • 物件(Object):用藍圖蓋出來的房子
  • 方法(Method):物件能做的動作
  • __init__:建構子,用來初始化資料
  • self:物件自己的指標,幫我們區分不同物件

那麼今天,我們要讓物件變得「更聰明、更有互動性」——
不只會自顧自跑程式,而是能互相連結、交換資料

還要學會超實用的觀念:繼承(Inheritance)

一、物件之間的互動:讓資料彼此連結

想像你在經營一間補習班:

  • 有學生(Student)
  • 有課程(Course)
  • 學生可以選課
  • 課程也能列出有哪些學生

這時候就出現「物件互動」:

不同物件之間彼此連結、共享資料。

最關鍵的觀念是:「資料同步!

如果用傳統函式寫法,要追蹤「誰選了哪門課」超容易出錯。
但透過物件導向思維,我們可以讓:

  • 學生自己管理「我選的課」
  • 課程自己管理「有哪些學生」

只要透過互相呼叫方法,就能讓資料自動同步!

範例:學生選課系統

class Course:
    def __init__(self, name):
        self.name = name
        self.students = []  # 課程裡的學生清單
    
    def add_student(self, student):
        self.students.append(student)
        print(f"{student.name} 選了課程 {self.name}")

    def show_students(self):
        print(f"課程 {self.name} 的學生有:")
        for s in self.students:
            print(f"- {s.name}")

class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
        self.courses = []  # 學生選的課程清單
    
    def enroll(self, course):
        self.courses.append(course)
        course.add_student(self)
        print(f"{self.name} 已成功加入 {course.name}")
    
    def show_courses(self):
        print(f"{self.name} 選的課程有:")
        for c in self.courses:
            print(f"- {c.name}")

# 建立學生
alice = Student("Alice", 5)
bob = Student("Bob", 6)

# 建立課程
math = Course("數學")
english = Course("英文")

# 學生選課
alice.enroll(math)
alice.enroll(english)
bob.enroll(math)

輸出:
https://ithelp.ithome.com.tw/upload/images/20251006/20164721v5Q4JVxUE1.png

說明:

  • alice.enroll(math)
    • Alice 把數學課加到自己的課程清單
    • 同時呼叫 math.add_student(alice) → 數學課也知道 Alice 選了它
    • 這就是物件之間互動!
  • 如果只更新學生或只更新課程,資料會不一致,初學者容易犯這種錯。(我當初就是這樣!)

⚠️新手常犯錯的地方:

  1. 只更新一邊資料
    → 導致學生記得有選課,課程卻沒記錄他。

  2. 忘記呼叫方法
    → 把 add_student 寫好卻沒執行,畫面沒任何變化。

查詢資料:

alice.show_courses()
bob.show_courses()

math.show_students()
english.show_students()

輸出會完整顯示:

  • 每個學生選的課程
  • 每個課程有哪些學生

輸出:

https://ithelp.ithome.com.tw/upload/images/20251006/20164721IDjOuisBib.png

二、繼承(Inheritance):減少重複、讓程式更乾淨

想像你有三個角色:

  • 學生(Student)
  • 老師(Teacher)
  • 職員(Staff)

他們都有「名字」和「年齡」,但各自又有不同功能(教課、讀書、打卡)。
若你三個類別都寫一遍 nameagesay_hello(),會重複超多程式碼!

如果還是有點不太清楚的話,這邊換個方式解說:

  • 想像 Person 是「人」的基本規格(name、age),學生(Student)與老師(Teacher)都是「人」,但會有額外屬性(學生有年級、老師有科目)。
  • 把共通的部分放 Person,學生與老師直接「繼承」,就不用在每個類別裡重複寫 name 的處理。

好處

  • 減少重複程式碼(DRY:Don't Repeat Yourself)
  • 如果需要修改共用行為,只改父類別就能影響所有子類別
  • 子類別可以拓展(增加屬性/方法)或覆寫(override)父類別的方法

範例:Person → Student & Teacher

class Person:
    def __init__(self, name):
        self.name = name
    
    def show_name(self):
        print(f"姓名:{self.name}")

class Student(Person):
    def __init__(self, name, grade):
        super().__init__(name)  # 呼叫父類別初始化 name
        self.grade = grade
    
    def show_info(self):
        print(f"學生: {self.name}, 年級: {self.grade}")

class Teacher(Person):
    def __init__(self, name, subject):
        super().__init__(name)
        self.subject = subject
    
    def show_info(self):
        print(f"老師: {self.name}, 教學科目: {self.subject}")

# 建立物件
s1 = Student("Alice", 5)
t1 = Teacher("Mr. Lee", "數學")

s1.show_name()   # 父類別方法
s1.show_info()   # 子類別方法

t1.show_name()
t1.show_info()

說明:

  • Person:父類別(共用屬性 name 與方法 show_name)。
  • Student(Person):表示 Student 繼承 Person。
    • super().__init__(name):呼叫父類別的初始化,把 name 初始化好(否則 self.name 會不存在)。
    • Student.total_students:類別變數(放在 class 層級),用來統計總學生數(注意:不是每個學生都有獨立 copy)。
  • Teacher(Person):同理,繼承 Person,並新增 subject

輸出:
https://ithelp.ithome.com.tw/upload/images/20251006/20164721sjQGC91ewm.png

新手陷阱:

  1. 忘記 super().__init__(name, age)

    → 子類別沒呼叫父類別建構子,就會缺屬性!

  2. super() 寫成 self.super() (常見錯誤)

我們再多看一種範例:

class Vehicle:  # 父類別(通用的交通工具)
    def __init__(self, brand):
        self.brand = brand

    def move(self):
        print(f"{self.brand} 正在移動中")

class Car(Vehicle):  # 子類別(汽車)
    def honk(self):
        print(f"{self.brand} 按喇叭:嗶嗶!")

class Bike(Vehicle):  # 子類別(腳踏車)
    def ring_bell(self):
        print(f"{self.brand} 按鈴:叮叮~")
  • Vehicle 是「父類別」,裡面定義了交通工具的基本功能。
  • Car(Vehicle) 表示 Car 繼承 Vehicle 的所有特性。
    • 所以 Car 會自動擁有 __init__move()
  • 子類別可以「額外增加自己的功能」,例如 Car 有 honk()、Bike 有 ring_bell()
car = Car("Toyota")
bike = Bike("Giant")

car.move()       # 從父類別繼承而來
car.honk()       # 子類別自己定義
bike.move()      # 從父類別繼承
bike.ring_bell() #子類別自己定義

輸出:
https://ithelp.ithome.com.tw/upload/images/20251006/20164721PR6ZgoeYPc.png

三、為什麼要繼承?

說來說去,看到這邊的你應該會有一個疑惑就是:

為什麼要繼承?

想像你要開一間補習班,會有:

  • 老師(Teacher)
  • 學生(Student)
  • 職員(Staff)

他們其實都有一些共通特性,像是:

  • 姓名(name)
  • 年齡(age)
  • 打招呼的方式(say_hello)

但他們又有一些「特別的功能」:

  • 老師會教課 teach()
  • 學生會讀書 study()
  • 職員會打卡 work()

如果你為每個角色都從頭寫一個 class
那這些「共通的部分」就得重複三次,非常浪費。

所以我們可以建立一個「父類別」── Person
然後讓 Teacher、Student、Staff 全部繼承自 Person

牛刀小試:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def say_hello(self):
        print(f"嗨,我是 {self.name},今年 {self.age} 歲!")

# 子類別繼承 Person
class Teacher(Person):
    def teach(self):
        print(f"{self.name} 正在上課中~")

class Student(Person):
    def study(self):
        print(f"{self.name} 正在努力讀書中 ")

# 測試
teacher = Teacher("王老師", 35)
student = Student("小明", 15)

teacher.say_hello()
teacher.teach()
student.say_hello()
student.study()

  • Teacher(Person) 表示 Teacher 是繼承自 Person。
  • 所以 Teacher 自動擁有了 Person 的 __init__say_hello()
  • 不用重寫,直接使用!

輸出:
https://ithelp.ithome.com.tw/upload/images/20251006/20164721LcYrGmZDvW.png

四、如果想「修改」繼承來的功能?

有時候子類別會想要「稍微改一下」父類別的功能。
這時候可以用「覆寫(override)」來取代父類別的方法。(延續上題)

class Student(Person):
    def say_hello(self):
        print(f"哈囉~我是學生 {self.name},我今年 {self.age} 歲喔!")

student = Student("小美", 14)
student.say_hello()  # 使用子類別版本,而非父類別的

「覆寫」就像是在繼承的基礎上「客製化」行為。
你還是「繼承」了那個功能的名字,只是裡面的內容換成你自己的版本。

輸出:
https://ithelp.ithome.com.tw/upload/images/20251006/20164721ZFUF1ZS75W.png

super():保留父類別功能,再加點料

有時候你想「在保留父類別原有功能的同時,再加一些東西」。這時候就要用到 super()

class Teacher(Person):
    def say_hello(self):
        super().say_hello()  # 先呼叫父類別的方法
        print("我是一位補習班老師")

teacher = Teacher("陳老師", 40)
teacher.say_hello()

輸出:
https://ithelp.ithome.com.tw/upload/images/20251006/20164721OQJbyE0vK4.png

五、實戰演練:點餐系統

# ====== 類別定義區 ======
class MenuItem:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    def info(self):
        print(f"{self.name} - ${self.price}")

class Drink(MenuItem):
    def __init__(self, name, price, size):
        super().__init__(name, price)
        self.size = size
    
    def info(self):
        print(f" {self.name} ({self.size}) - ${self.price}")

class MainDish(MenuItem):
    def __init__(self, name, price, spicy=False):
        super().__init__(name, price)
        self.spicy = spicy
    
    def info(self):
        spicy_mark = "🌶️" if self.spicy else ""
        print(f" {self.name}{spicy_mark} - ${self.price}")

class Dessert(MenuItem):
    def __init__(self, name, price, sweetness):
        super().__init__(name, price)
        self.sweetness = sweetness
    
    def info(self):
        print(f"{self.name}(甜度:{self.sweetness}/5) - ${self.price}")

# ====== 建立菜單 ======
menu = [
    Drink("紅茶", 30, "M"),
    Drink("拿鐵", 60, "L"),
    MainDish("咖哩飯", 120, True),
    MainDish("燉飯", 150, False),
    Dessert("布朗尼", 80, 4),
    Dessert("抹茶蛋糕", 90, 3)
]

# ====== 主程式互動區 ======
print("=== 🍽️ 歡迎光臨IT人餐廳!===")

order_list = []

while True:
    print("\n--- 今日菜單 ---")
    for i, item in enumerate(menu, start=1):
        print(f"{i}. ", end="")
        item.info()

    choice = input("\n請輸入想點的餐點編號(輸入 q 結束點餐):")

    if choice.lower() == "q":
        break

    if not choice.isdigit() or not (1 <= int(choice) <= len(menu)):
        print("⚠️ 無效輸入,請重新輸入餐點編號。")
        continue

    item = menu[int(choice) - 1]
    order_list.append(item)
    print(f"✅ 已將「{item.name}」加入餐點。")

# 結帳
print("\n===🧾結帳明細 ===")
total = 0
for o in order_list:
    o.info()
    total += o.price

print(f"\n💵 總金額:${total}")
print("感謝您的光臨")

我們先定義一個父類別 MenuItem,代表菜單上的每一項。

每個項目都有 名稱價格

接著,我們創建三個子類別:

  • Drink(飲料)→ 有「大小杯」資訊
  • MainDish(主餐)→ 可以標註是否「辣」
  • Dessert(甜點)→ 有「甜度」資訊

輸出:
https://ithelp.ithome.com.tw/upload/images/20251006/20164721PNNBdHpCpR.png

完整輸出:
https://ithelp.ithome.com.tw/upload/images/20251006/201647214ae5JGNyMn.png

再次提醒新手常見錯誤:

self.super().__init__(name, price) 
 # 這樣寫是錯的!

因為 super() 是一個內建函式,不需要加 self.

正確寫法:

super().__init__(name, price)
#這樣才是正確的

結語:從「繼承」到「擴充」的感覺

老實說,當初我剛學「繼承」時超級懷疑人生。

心想:「我就直接寫三個類別不行嗎?幹嘛搞這麼複雜?」
但當你開始實作像這種「餐廳系統」時,就會突然懂了:

原來繼承的重點不是省幾行程式碼,

而是讓整個系統有「延展性」與「一致性」。

今天多加一個類別,比如「湯品」,
我根本不用改前面的架構,只要繼承 MenuItem,加個新屬性就搞定。
這樣也很省時對吧!!

今天辛苦啦!希望讀者有跟上進度~這邊確實會有點抽象!!

不過多看幾次、多試幾次,就會進步很多的!!

一起加油~迎接最後一週!!時間過超快的!

那麼我們就明天見囉!!


上一篇
【Day21】類別與物件入門:讓你的程式更有組織力
下一篇
【Day23】物件再進化:多層繼承、多重繼承與私有方法
系列文
Python 小白的逆襲:30 天從零到能教人的精華筆記,寫給迷惘的你與當年的我自己!24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言