2026年11月2日 星期一

Python 例外處理實戰:飲料店 POS 點餐系統

Python 程式實習|整合實作

Python 例外處理實戰:打造不怕輸入錯誤的飲料店 POS 點餐系統

顧客可能輸入錯誤的商品代碼、把「兩杯」當成數字、輸入負數杯數,甚至付款不足。真正可靠的程式,不是永遠不會遇到錯誤,而是能辨識可預期的錯誤,給出提示並讓使用者重新操作。本篇從生活情境出發,整合字典、迴圈、函式、類別、例外處理,完成可在終端機執行的飲料店 POS。

一、情境與學習目標

店裡販售紅茶(30 元)、珍珠奶茶(50 元)、綠茶(35 元)。顧客可重複選購商品、輸入杯數,輸入 0 結帳;系統列出明細、計算總額、收款及找零。

完成後,你應能回答:哪些錯誤應使用 try/except?哪些情況要用 if 驗證?為什麼把輸入流程與訂單資料分開設計?

二、先看系統流程

顯示菜單輸入代碼驗證資料加入訂單結帳收款輸入錯誤 → 重新輸入繼續點餐 → 回到菜單

三、第一步:用字典建立菜單

MENU = {"1": ("紅茶", 30),
        "2": ("珍珠奶茶", 50),
        "3": ("綠茶", 35)}

商品代碼使用字串,商品資料以 (名稱, 單價) 表示。透過 MENU[code] 可以找到對應商品;如果代碼不存在,應該提供清楚的錯誤訊息,而不是讓程式直接崩潰。

四、第二步:先處理最常見的輸入錯誤

def read_quantity():
    while True:
        try:
            quantity = int(input("數量(杯):"))
            if quantity <= 0:
                raise ValueError("數量必須大於 0")
            return quantity
        except ValueError as error:
            print(f"輸入錯誤:{error},請重新輸入。")

int() 遇到「兩杯」會產生 ValueError;數量為 0 或負數則是我們定義的商業規則,使用 raise ValueError(...) 主動拒絕。兩者都由同一個 except 處理,再用迴圈重新詢問。

五、第三步:用類別管理訂單

class Order:
    def __init__(self):
        self.items = []

    def add_item(self, code, quantity):
        if code not in MENU:
            raise KeyError("找不到這個商品代碼")
        if quantity <= 0:
            raise ValueError("數量必須大於 0")
        name, price = MENU[code]
        self.items.append((name, price, quantity))

    def total(self):
        return sum(price * qty for _, price, qty in self.items)

Order 負責保存訂單和計算金額;add_item() 再次檢查資料,避免其他程式繞過輸入介面時加入不合理的商品。這就是封裝與例外處理的結合。

六、第四步:結帳時處理付款不足

def checkout(order):
    total = order.total()
    while True:
        try:
            paid = int(input(f"應收 {total} 元,請輸入實收金額:"))
            if paid < total:
                raise ValueError("實收金額不足")
        except ValueError as error:
            print(f"付款失敗:{error},請重新輸入。")
        else:
            print(f"找零:{paid - total}元")
            print("交易完成,謝謝光臨!")
            return

這裡的 else 只有在 try 沒有產生例外時才執行。實收金額不足時不會結帳,而是要求重新輸入。這是課堂示範的現金收款模型,尚未包含折扣、發票或交易紀錄。

七、完整可執行程式

將以下程式存成 drink_pos.py,在終端機輸入 python drink_pos.py 執行。不需要額外安裝套件。

"""飲料店 POS:以教學為目的的命令列範例。"""
MENU = {"1": ("紅茶", 30), "2": ("珍珠奶茶", 50), "3": ("綠茶", 35)}

class Order:
    def __init__(self):
        self.items = []

    def add_item(self, code, quantity):
        if code not in MENU:
            raise KeyError("找不到這個商品代碼")
        if quantity <= 0:
            raise ValueError("數量必須大於 0")
        name, price = MENU[code]
        self.items.append((name, price, quantity))

    def total(self):
        return sum(price * qty for _, price, qty in self.items)

    def show_receipt(self):
        print("\n=== 訂單明細 ===")
        for name, price, qty in self.items:
            print(f"{name} {qty}杯 × {price}元 = {price * qty}元")
        print(f"合計:{self.total()}元")


def read_quantity():
    while True:
        try:
            quantity = int(input("數量(杯):"))
            if quantity <= 0:
                raise ValueError("數量必須大於 0")
            return quantity
        except ValueError as error:
            print(f"輸入錯誤:{error},請重新輸入。")


def checkout(order):
    total = order.total()
    while True:
        try:
            paid = int(input(f"應收 {total} 元,請輸入實收金額:"))
            if paid < total:
                raise ValueError("實收金額不足")
        except ValueError as error:
            print(f"付款失敗:{error},請重新輸入。")
        else:
            print(f"找零:{paid - total}元")
            print("交易完成,謝謝光臨!")
            return


def main():
    order = Order()
    print("=== Python 飲料店 POS ===")
    while True:
        print("\n菜單:")
        for code, (name, price) in MENU.items():
            print(f"{code}. {name} {price}元")
        code = input("商品代碼(0 結帳):").strip()
        if code == "0":
            if not order.items:
                print("目前沒有商品,結束程式。")
                return
            break
        try:
            if code not in MENU:
                raise KeyError("沒有這個商品代碼")
            qty = read_quantity()
            order.add_item(code, qty)
        except KeyError as error:
            print(f"選購失敗:{error.args[0]},請重新選擇。")
        else:
            print("商品已加入訂單。")
    order.show_receipt()
    checkout(order)


if __name__ == "__main__":
    main()

八、執行測試:讓錯誤真的發生

=== Python 飲料店 POS ===
菜單:
1. 紅茶 30元
2. 珍珠奶茶 50元
3. 綠茶 35元
商品代碼(0 結帳):9
選購失敗:沒有這個商品代碼,請重新選擇。

商品代碼(0 結帳):2
數量(杯):兩杯
輸入錯誤:invalid literal for int() with base 10: '兩杯',請重新輸入。
數量(杯):-1
輸入錯誤:數量必須大於 0,請重新輸入。
數量(杯):2
商品已加入訂單。

商品代碼(0 結帳):0
=== 訂單明細 ===
珍珠奶茶 2杯 × 50元 = 100元
合計:100元
應收 100 元,請輸入實收金額:50
付款失敗:實收金額不足,請重新輸入。
應收 100 元,請輸入實收金額:200
找零:100元
交易完成,謝謝光臨!

上方為示意互動紀錄,實際終端機畫面會依輸入而異。

九、程式中的例外處理與條件判斷

情境處理方式原因
輸入「兩杯」except ValueErrorint() 無法轉換
輸入 -1 杯if + raise ValueError違反購買數量規則
商品代碼 9raise KeyError + except菜單沒有此商品
付款不足if + raise ValueError違反付款規則
正常付款else沒有發生例外才完成交易
重要觀念:不是所有錯誤都應該用 except 處理。程式設計時,先以條件判斷描述正常的業務規則,再用例外表示無法完成操作的情況。也不要使用 except: pass 把問題全部隱藏。

十、三階段 AI 協作實習

No-AI|獨立理解

不使用 AI,畫出點餐流程,指出至少三個可能發生錯誤的地方,並說明 while、try、except 各自負責什麼。

AI Pair|共同改善

請 AI 協助新增「同商品累計杯數」及「輸入 q 取消交易」功能。學生必須自行測試空訂單、非法代碼與取消操作。

Challenge AI|驗證 AI

要求 AI 將 POS 擴充成「庫存有限」的版本。測試庫存不足、輸入 0 杯、超額付款與連續輸入錯誤,並檢查是否發生先扣庫存、後來卻結帳失敗的資料不一致問題。

十一、延伸挑戰與教學反思

本範例刻意保持精簡,讓學生先理解錯誤發生時如何恢復。下一步可增加商品庫存、訂單編號、檔案儲存、GUI 或 Django 網頁介面。正式 POS 還需要考慮金額精度、資料持久化、交易一致性與權限管理。

AI 可以快速寫出 try/except,但真正的程式設計能力,是判斷什麼情況應重試、拒絕、取消或回復資料。當學生能清楚回答這些問題,就不只是會寫語法,而是開始學會設計可靠的系統。

Python 程式實習|生活化教學 × 物件導向 × 例外處理 × AI 協作

Python 例外處理:從飲料店點餐學會 try、except

Python 程式實習|生活情境 × AI 協作

Python 例外處理(Exception Handling):當程式發生錯誤,如何讓它繼續運作?

從飲料店買珍珠奶茶、朋友聚餐分帳,到智慧養殖感測器,逐步理解 try、except、else、finally 與 raise。

一、生活情境:到飲料店買珍珠奶茶

每杯珍珠奶茶 50 元,請顧客輸入購買杯數:

price = 50
quantity = int(input("請問要買幾杯珍珠奶茶?"))
total = price * quantity
print("總金額:", total, "元")

輸入 3,結果是 150 元;但輸入「三杯」會發生 ValueError,因為 int() 無法將這段文字轉成整數。

重點:輸入資料不符合程式預期,不應讓整個點餐流程毫無說明地中斷。

二、第一個例外處理:try 與 except

price = 50
try:
    quantity = int(input("請問要買幾杯珍珠奶茶?"))
    total = price * quantity
    print("總金額:", total, "元")
except ValueError:
    print("輸入錯誤!請輸入數字。")

try 放可能產生例外的程式;except ValueError 負責處理指定的轉換錯誤。其他未被捕捉的例外仍可能中斷程式。

try:執行程式發生 ValueError?否是正常完成 tryexcept:處理錯誤

三、輸入錯誤就重新輸入

加入迴圈,讓顧客可以重新輸入。注意:輸入負數不會自動產生 ValueError,必須另外檢查。

price = 50
while True:
    try:
        quantity = int(input("請問要買幾杯?"))
        if quantity <= 0:
            print("購買數量必須大於 0")
            continue
        break
    except ValueError:
        print("請輸入正確的整數!")

total = price * quantity
print("總金額:", total, "元")
請問要買幾杯?三杯
請輸入正確的整數!
請問要買幾杯?-2
購買數量必須大於 0
請問要買幾杯?3
總金額: 150 元

四、聚餐分帳:認識不同例外

如果 1,200 元餐費要平均分攤,輸入「四個人」會有轉換問題,輸入 0 則會發生除以零的例外。

total = 1200
try:
    people = int(input("請輸入分帳人數:"))
    if people < 0:
        print("人數不能是負數")
    else:
        average = total / people
        print("每人應付:", average)
except ValueError:
    print("請輸入整數!")
except ZeroDivisionError:
    print("分帳人數不能是 0!")
例外典型原因生活情境
ValueError值無法正確轉換杯數輸入「三杯」
ZeroDivisionError除以零分帳人數輸入 0
FileNotFoundError指定檔案不存在找不到成績檔
IndexError索引超出範圍讀取不存在的第 5 筆資料
KeyError字典沒有指定鍵查詢不存在的商品代碼
TypeError運算型別不相容字串與整數直接相加

五、讀取成績檔:FileNotFoundError

try:
    with open("scores.txt", "r", encoding="utf-8") as file:
        content = file.read()
        print(content)
except FileNotFoundError:
    print("找不到成績檔案!")

with open() 能在離開區塊時自動關閉檔案,比手動呼叫 close() 更可靠。

六、try、except、else、finally

try:
    number = int(input("請輸入一個數字:"))
except ValueError:
    print("輸入錯誤!")
else:
    print("輸入成功:", number)
finally:
    print("程式處理結束")
語法作用
try執行可能產生例外的程式
except捕捉並處理指定例外
else當 try 沒有發生例外時執行
finally離開處理結構時通常會執行,適合清理資源

七、raise:自己主動提出例外

當輸入數值雖然是整數,卻不符合商業規則時,可使用 raise。

def order_drink(quantity):
    if quantity <= 0:
        raise ValueError("購買數量必須大於 0")
    return quantity * 50

try:
    total = order_drink(-2)
    print("總金額:", total)
except ValueError as error:
    print("訂購失敗:", error)
訂購失敗: 購買數量必須大於 0

八、整合物件導向:智慧養殖感測資料檢查

延續前面 Class 與封裝的課程,讓養殖池物件檢查溶氧數值。此處使用 None 表示尚未取得有效資料。

class Pond:
    def __init__(self, name):
        self.name = name
        self.oxygen = None

    def update_oxygen(self, value):
        oxygen = float(value)
        if oxygen < 0:
            raise ValueError("溶氧不能小於 0")
        self.oxygen = oxygen

pond1 = Pond("水井一號池")
try:
    pond1.update_oxygen("-5")
except ValueError as error:
    print("感測資料錯誤:", error)
print("目前溶氧:", pond1.oxygen)
感測資料錯誤: 溶氧不能小於 0
目前溶氧: None

這裡的數值與規則只是教學示例。正式 AIoT 系統還需要依感測器規格、現場環境及養殖需求進行驗證。

九、AI 協作實習:No-AI、AI Pair、Challenge AI

No-AI:預測錯誤

先不使用 AI,測試下列程式輸入 ABC、0 與有效整數的結果,並自行加入例外處理。

a = int(input("第一個數字:"))
b = int(input("第二個數字:"))
print(a / b)

AI Pair:請 AI 協助改善

提示詞:「請幫我設計一個 Python 飲料店點餐程式,使用 try、except、else、finally,處理輸入非整數、購買數量小於等於零等情況。請逐步解釋語法,並保持程式簡單。」

Challenge AI:最多輸入錯誤三次

新增需求:顧客最多可以輸入錯誤三次,第三次失敗即取消訂單。先自行設計流程,再與 AI 合作完成。測試第一次成功、錯誤兩次後成功、連續錯誤三次等情況。

十、例外處理不是隱藏錯誤

不要隨意使用沒有指定類型的 except:,更不要用 except: pass 靜默忽略所有問題。應優先捕捉預期的例外,提供清楚訊息;無法安全恢復的錯誤應適當記錄或向上傳遞。

核心觀念:例外處理不是保證程式永遠不出錯,而是讓程式在預期的錯誤發生時,能採取清楚、可控制的應對方式。

十一、結語:從生活中的錯誤,設計更可靠的程式

飲料店點餐、聚餐分帳、成績檔案與智慧養殖感測器,都是理解例外處理的好起點。AI 可以快速寫出 try 與 except,但學生仍需要判斷哪些錯誤可以恢復、哪些應該停止,以及哪些不能被忽略。

下一個實習:Python 例外處理實戰——打造不怕輸入錯誤的飲料店 POS 點餐系統。

2026年11月1日 星期日

Capstone 問題診斷導向教學:Gate A、Gate B、Gate C 三大決策關卡

CAPSTONE PROJECT · PROBLEM DIAGNOSIS

從議題探索到專題成立:認識 Gate A、Gate B、Gate C 三大決策關卡

問題診斷導向 Capstone 教學文章|適用於不同產業、組織、社區及跨域合作場域

在 Capstone(總整課程/專題實作)中,學生常急著決定要開發網站、App、智慧設備或 AI 應用,卻未必先確認真實工作情境中的問題是否存在、為何持續發生,以及是否適合在課程期間完成。問題診斷導向的核心,就是先以訪談、觀察與工作證據理解問題,再把經過驗證的問題轉化為可執行的專題。

為避免各階段過早跳到解決方案,教學流程設置三個階段性決策關卡(Decision Gates):Gate A、Gate B、Gate C。每一關都要回答不同的問題,且未通過時應補足資料或調整方向,而不是直接進入下一步。

一句話記憶:Gate A 選對議題;Gate B 找對問題;Gate C 確認專題能夠執行與驗證。

一、三個 Gate 有什麼不同?

比較項目Gate AGate BGate C
核心提問這個議題值得進一步探索嗎?是否找到有證據支持的跨角色系統性核心問題?能否轉成學生可執行、可驗證的課程專題?
主要依據高階訪談、問題線索、候選議題比較與合作條件執行層訪談、旅程圖、工作流程圖、關鍵資訊流圖與六要素POV、介入位置、HMW、教學可行性與責任邊界
產出焦點議題與場域探索邊界跨角色系統性核心問題課程專題成立/調整判定
通過後規劃執行層訪談及場域探索萃取 Needs、Insight、POV定錨正式課程挑戰任務
未通過重新比較議題、釐清範圍或合作條件回到角色別三圖證據與問題群集調整介入範圍、任務難度、資源或責任邊界

二、Gate A:焦點議題確認

Gate A 是「選定值得深入研究的議題」,而不是宣告已經找到問題根因。在前期高階訪談後,先整理受訪者提及的具體困難、反覆現象、工作負擔或風險,形成問題線索,再依共同工作情境分群為候選議題。接著比較各議題的重要性、探索價值、教學適切性、教學可行性及場域合作可行性。

Gate A 通過的三項條件

  • 已形成明確且具探索價值的焦點議題。
  • 已界定探索範圍與合作邊界。
  • 已確認具備後續執行層訪談與場域探索的可行性。

通用示例:訪談顯示某項跨部門服務需要多次協調,於是先把「跨角色服務協作與資訊傳遞」列為焦點議題;此時仍不能斷定原因就是資訊不完整、工具不足或人員疏失。這些都要留待現場查證。

教學提醒:焦點議題是探索主題,不是系統性核心問題,更不是解決方案。

三、Gate B:跨角色系統性核心問題確認

通過 Gate A 後,教師與學生要進入實際工作情境,理解至少兩個核心角色如何共同完成任務。透過Journey Map(角色旅程圖)、Workflow Map(工作流程圖)、Information Flow Map(關鍵資訊流圖),交叉比對體驗、行為與資訊承接的證據。

接著整合六項核心問題元素:主要角色、共同關鍵情境、共同關鍵任務、共同形成機制、共同問題現象、共同影響。只有當角色別證據指向同一形成機制,才有基礎提出跨角色系統性核心問題。

Gate B 四項品質檢核

  • 兩個角色的問題群集是否指向同一跨角色形成機制?
  • 六項核心問題元素是否都有旅程圖、流程圖、資訊流圖的證據支持?
  • 核心問題是否未預設工具、系統或 AI 解方?
  • 是否可進入 Needs、Insight 與 POV?

通用示例:若兩個角色都在同一服務交接流程中反覆補問與確認,學生不能只因兩人都說「溝通麻煩」就判定為共同問題;還須查明哪個交接節點、哪種必要資訊或完成條件未被充分承接,以及實際造成什麼工作影響。

教學提醒:訪談中的推測不是已確認的形成機制;示範案例、AI 生成的情節與真實現場證據也必須清楚區分。

四、Gate B 之後:Needs、Insight 與 POV

Gate B 通過後,進入設計觀點形成階段。這三個概念有明確分工:

概念回答的問題撰寫重點
Needs(需求)角色要完成關鍵任務,缺少什麼必要工作條件?寫工作條件,不寫工具功能。
Insight(洞察)為何已有因應作法,問題仍持續發生?連結既有作法、形成機制、重複現象及影響。
POV(設計觀點)兩個角色共同需要改善的問題切面是什麼?整合核心角色、共同情境、共同需求及共同洞察。

POV 句型:【核心角色】需要在【共同關鍵情境】下,能夠【共同關鍵需求】,因為【共同洞察】。

POV 還要經過五項品質檢核:是否對應兩角色需求、說明共同形成機制、聚焦同一資訊承接鏈、未預設工具或 AI 解方,以及能否進入介入位置與 HMW。

五、Gate C:課程專題成立

經過 POV 品質檢核後,才進一步判斷應介入哪個工作環節,並形成 HMW(How Might We,我們如何能夠……?)設計問題。教材以三個可介入位置協助思考:L1 資料產生層、L2 資訊流與交接層、L3 判斷使用層。它們是設計介入位置,不是另一套問題診斷模型。

接著確認學生的學習能力、跨域整合需求、場域合作資源、資料與驗證條件,以及專業責任邊界。若使用 AI,也必須明確界定 AI 的輔助範圍,不應把 AI 視為唯一解方,更不能以學生原型取代應由專業人員負責的判斷。

Gate C 課程成立性檢核重點

  • 是否具備真實問題證據與產業/場域需求?
  • 是否具有跨角色或跨領域的學習價值?
  • 是否符合學生先備能力及課程時間?
  • 是否能完成原型、情境測試或其他可行驗證?
  • 是否具備必要的場域、人員、資料與合作支持?
  • 是否清楚界定學生、AI 與領域專業人員的責任?

通用示例:即使已確認跨角色資訊承接問題,也不能直接指定學生開發一套完整管理系統。若課程僅有一學期,可能先以流程重整、表單原型、介面模擬或情境測試進行驗證,再評估是否需要進一步導入技術。

六、完整教學流程圖

高階訪談 → 問題線索 → 候選議題比較
↓
Gate A|焦點議題是否成立?
↓
執行層訪談與場域觀察 → 三張分析圖 → 六要素核心問題
↓
Gate B|跨角色系統性核心問題是否成立?
↓
Needs → Insight → POV → POV 品質檢核
↓
介入位置 L1/L2/L3 → HMW → 課程成立性與責任邊界
↓
Gate C|課程專題是否成立?
↓
正式 Capstone 課程挑戰任務 → 原型與驗證

七、教師帶領學生時,最常見的三個誤區

  1. 把候選議題當成問題根因:高階訪談指出「流程不順」,只表示值得查證;真正形成機制仍需執行層證據。
  2. 把需求寫成工具:「需要一個 AI 平台」是可能的解方;「需要在交接時取得完整且可確認的資訊」才是工作需求。
  3. 把做得出來當成值得做:技術可行不代表專題成立;還須確認問題證據、場域價值、學生學習與驗證條件。

八、課堂討論與反思

  1. 如果合作單位希望學生「做一個 App」,教師應先用哪個 Gate 的思考方式回應?為什麼?
  2. 兩個角色都抱怨資訊傳遞麻煩,是否足以通過 Gate B?還需要哪些證據?
  3. 若核心問題已確認,但場域無法提供測試資料,Gate C 應如何調整專題範圍?

Gate A 選對議題 → Gate B 找對問題 → Gate C 做成可執行、可驗證的專題。

三個 Gate 並不是為了增加行政程序,而是讓學生每走一步都有依據:先確認值得探索,再確認問題成立,最後確認課程可做且能驗證。這也是問題診斷導向 Capstone 與「先決定技術、再尋找應用情境」最大的差異。

教材依據:《Capstone 專題問題診斷導向備課工作坊_20261031(智慧照護)_虎科場Final.pdf》。本文以跨產業、跨機構的通用教學語境重新編寫;通用示例僅作教學說明,並非特定機構的實際診斷結論。

2026年10月31日 星期六

Python 物件導向實戰:智慧養殖監控系統

Python 物件導向實戰:用 Class、繼承、多型與封裝設計一套智慧養殖監控系統

Python 程式實習系列(五)|從水井村智慧養殖情境出發,整合四大物件導向觀念,並透過 No-AI、AI Pair、Challenge AI 練習 AI 協作設計。

一、AI 已經會寫程式,為什麼還要做整合實作?

當 AI 可以一次生成完整的 Python 類別,學習目標就不應只是「打出正確語法」,而是能解釋:哪些資料需要保護?哪些功能應共用?新型養殖池加入時,舊程式是否需要修改?本篇以水井村智慧養殖為情境,從設計到測試建立可執行的入門監控模擬系統。

二、需求分析:先決定系統要做什麼

這是一個離線模擬程式,不連接真實感測器。系統要管理一般池、智慧池與魚菜共生池;各池都有名稱與溶氧。智慧池另外有溶氧警戒值,魚菜共生池再增加水位。系統必須拒絕負數溶氧、允許更新感測數據,並使用同一個迴圈列出不同類型養殖池的狀態。

三、先畫出繼承階層圖

由下圖可見,SmartPond 是 Pond 的一種;AquaponicsPond 又是 SmartPond 的一種。這是為了教學而採用的簡化架構。實務上若魚菜共生池的規則與智慧池差異很大,也可以考慮改用組合而非繼承。
Pondoxygen / check_status() SmartPondlimit / 覆寫 check_status() AquaponicsPondwater_level / 覆寫 check_status() 共同屬性與方法新增警戒規則新增水位監測

圖:教學用的三層繼承階層;三種物件都能呼叫 check_status()。

四、完整 Python 程式:四大觀念一次整合

將下方程式存成 pond_oop_demo.py,以 Python 3 執行。範例不需額外安裝套件。

class Pond:
    def __init__(self, name, oxygen):
        self.name = name
        self.oxygen = oxygen

    @property
    def oxygen(self):
        return self._oxygen

    @oxygen.setter
    def oxygen(self, value):
        if not isinstance(value, (int, float)) or isinstance(value, bool):
            raise TypeError("溶氧必須是數字")
        if not 0 <= value <= 25:
            raise ValueError("溶氧須介於 0~25 mg/L")
        self._oxygen = float(value)

    def check_status(self):
        return f"{self.name}:溶氧 {self.oxygen:.1f} mg/L"


class SmartPond(Pond):
    def __init__(self, name, oxygen, limit=5.0):
        super().__init__(name, oxygen)
        if limit < 0:
            raise ValueError("警戒值不可為負數")
        self.limit = limit

    def check_status(self):
        status = "偏低,請檢查增氧設備" if self.oxygen < self.limit else "正常"
        return f"{self.name}:溶氧 {self.oxygen:.1f} mg/L,{status}"


class AquaponicsPond(SmartPond):
    def __init__(self, name, oxygen, water_level, limit=5.0):
        super().__init__(name, oxygen, limit)
        self.water_level = water_level

    @property
    def water_level(self):
        return self._water_level

    @water_level.setter
    def water_level(self, value):
        if not isinstance(value, (int, float)) or isinstance(value, bool):
            raise TypeError("水位必須是數字")
        if value < 0:
            raise ValueError("水位不可為負數")
        self._water_level = float(value)

    def check_status(self):
        oxygen_status = super().check_status()
        level_status = "水位偏低" if self.water_level < 30 else "水位正常"
        return f"{oxygen_status};{level_status}({self.water_level:.0f} cm)"


ponds = [
    Pond("一般池", 6.1),
    SmartPond("智慧池", 4.2),
    AquaponicsPond("魚菜共生池", 5.8, 25),
]

for pond in ponds:
    print(pond.check_status())

print("\n更新智慧池溶氧後:")
ponds[1].oxygen = 5.6
print(ponds[1].check_status())

print("\n測試不合理資料:")
try:
    ponds[1].oxygen = -1
except ValueError as error:
    print("拒絕更新:", error)

五、執行結果與程式解讀

程式建立三種物件,透過相同的 check_status() 呼叫顯示不同訊息;接著更新智慧池溶氧,最後刻意輸入 -1,確認資料驗證會拒絕不合理數值。

一般池:溶氧 6.1 mg/L
智慧池:溶氧 4.2 mg/L,偏低,請檢查增氧設備
魚菜共生池:溶氧 5.8 mg/L,正常;水位偏低(25 cm)

更新智慧池溶氧後:
智慧池:溶氧 5.6 mg/L,正常

測試不合理資料:
拒絕更新: 溶氧須介於 0~25 mg/L

六、Class、繼承、多型、封裝在哪裡?

觀念程式位置設計意義
Class 類別class Pond組織名稱、溶氧與狀態查詢
Inheritance 繼承SmartPond(Pond)沿用共同資料與方法,再增加警戒功能
Polymorphism 多型pond.check_status()同一呼叫,依物件類型執行不同方法
Encapsulation 封裝@property、@oxygen.setter在公開屬性寫入時檢查資料有效性

七、No-AI → AI Pair → Challenge AI

No-AI:先預測再執行

不使用 AI,畫出三個類別的關係,指出哪一個方法被覆寫;預測三個物件的輸出,並解釋為何負數溶氧被拒絕。

AI Pair:請 AI 當程式碼審查者

提示詞:「請審查這份 Python 智慧養殖監控程式,指出繼承是否合理、封裝是否完整、多型是否有幫助,並提出三項可測試的改善建議。先不要直接重寫程式。」

Challenge AI:新增需求

新增 ShrimpPond,要求不同的溶氧警戒值;再增加溫度屬性與溫度異常警告。學生先畫類別圖、寫測試案例,再請 AI 協助實作。檢查是否能在不修改原本巡覽迴圈的情況下加入新類別。

八、進一步連接 AIoT:從模擬走向真實系統

完成本篇後,可讓 Raspberry Pi Pico W 或 ESP32 負責感測資料取得,再透過 MQTT 或 HTTP 傳送至 Python 程式。此時可以把「感測器資料來源」獨立為物件,讓養殖池類別專注於狀態與規則;避免讓一個類別同時負責網路、資料庫、感測器與警報的全部工作。

九、測試與安全邊界

本篇的溶氧 0~25 mg/L、警戒值 5 mg/L 與水位 30 cm 都是教學示例,不是現場控制建議。真實系統須依物種、設備量測範圍、校正與現場條件決定閾值;感測器斷線、資料缺漏、時間戳記與設備故障也需要另外處理。

此外,Python 的底線屬性是慣例,@property 不是防止惡意修改的安全機制。它主要協助我們維持正常程式使用情境下的資料一致性。

十、結語:AI 寫程式,學生設計系統

Class 讓我們定義物件;繼承讓相似物件共用設計;多型讓不同物件以一致方式操作;封裝則幫助維持資料合理性。真正的學習成果,不只是程式能執行,而是能回答「為什麼這樣設計?」並能在需求改變時提出合理修改。

關鍵字:Python、OOP、Class、Inheritance、Polymorphism、Encapsulation、AIoT、智慧養殖、AI 協作

Python 封裝:為什麼不應任意修改物件資料?

Python 封裝:為什麼不應任意修改物件資料?

Python 封裝(Encapsulation):為什麼不應該讓所有程式都能隨意修改物件資料?

Python 物件導向系列(四)|Class × Inheritance × Polymorphism × Encapsulation

AI 能快速寫出 Python 類別,但真正的設計能力,是知道哪些資料需要檢查、哪些操作應該被允許,以及如何讓物件維持合理的狀態。

前面三篇已介紹類別、繼承與多型。這次繼續以「水井村智慧養殖池」為例,理解封裝(Encapsulation)的用意。封裝不只是把資料藏起來,更是把資料與相關操作組織在一起,提供清楚的存取介面與檢查規則。

一、生活中的封裝:自動販賣機

使用者可以選商品、付款與購買,但不能任意修改販賣機的庫存帳務。操作必須經過規定的介面;程式中的物件也可以如此設計。

二、沒有檢查的公開屬性,會發生什麼事?

class Pond:
    def __init__(self, name, oxygen):
        self.name = name
        self.oxygen = oxygen

pond1 = Pond("水井一號池", 5.8)
pond1.oxygen = -100
print(pond1.oxygen)

執行結果:

-100

溶氧濃度不應為負數,但這段程式仍允許寫入。當感測數據用於警報或控制時,資料錯誤可能引發後續問題。

三、第一步:透過方法更新資料

class Pond:
    def __init__(self, name, oxygen):
        self.name = name
        self.oxygen = 0
        self.set_oxygen(oxygen)

    def set_oxygen(self, value):
        if value >= 0:
            self.oxygen = value
        else:
            print("錯誤:溶氧不能小於 0")

    def show_info(self):
        print(self.name, "溶氧:", self.oxygen)

pond1 = Pond("水井一號池", 5.8)
pond1.set_oxygen(6.2)
pond1.set_oxygen(-100)
pond1.show_info()
錯誤:溶氧不能小於 0
水井一號池 溶氧: 6.2

這個版本可以檢查經過 set_oxygen() 的更新,但外部仍能執行 pond1.oxygen = -100 繞過檢查,因此需要更清楚的屬性介面。

四、Python 的屬性命名與封裝

寫法意義注意事項
self.oxygen公開屬性通常可直接讀寫
self._oxygen非公開慣例提醒使用者避免從外部直接修改
self.__oxygen名稱改寫降低意外存取及子類別名稱衝突,並非絕對私有

Python 的雙底線屬性會發生 Name Mangling(名稱改寫),例如 __oxygen 可能成為 _Pond__oxygen。這不是安全邊界,也不能用來防止惡意程式存取。

五、推薦實作:使用 @property 驗證資料

class Pond:
    def __init__(self, name, oxygen):
        self.name = name
        self.oxygen = oxygen

    @property
    def oxygen(self):
        return self._oxygen

    @oxygen.setter
    def oxygen(self, value):
        if value < 0:
            raise ValueError("溶氧不能小於 0")
        self._oxygen = value

pond1 = Pond("水井一號池", 5.8)
print(pond1.oxygen)
pond1.oxygen = 6.2
print(pond1.oxygen)
5.8
6.2

此時 pond1.oxygen = 6.2 看似直接指定屬性,實際上會呼叫 @oxygen.setter 的檢查邏輯。若執行 pond1.oxygen = -100,則會引發 ValueError。

重點:_oxygen 仍可被直接存取。封裝的目的是建立可靠、清楚的操作介面,並非提供資訊安全上的絕對保護。

六、封裝流程圖:資料如何被檢查?

外部程式pond.oxygen = 值@oxygen.setter檢查 value ≥ 0合理數值更新 _oxygen不合理數值引發 ValueError

使用者只需要知道如何操作公開介面,物件內部則負責檢查與維持資料一致性。

七、所有屬性都要用 Getter 與 Setter 嗎?

不需要。如果只是簡單的學生姓名,直接使用公開屬性就很合理:

class Student:
    def __init__(self, name):
        self.name = name

只有在需要驗證、轉換、唯讀控制或維持特殊規則時,才考慮 @property 或明確的操作方法。好的封裝不是讓程式變長,而是讓規則更明確。

八、No-AI → AI Pair → Challenge AI 實習

No-AI:先發現資料問題

class Product:
    def __init__(self, name, stock):
        self.name = name
        self.stock = stock

product = Product("水井文蛤", 100)
product.stock = -50

請思考:負數庫存有什麼問題?如果直接修改庫存,哪些規則可能被跳過?

AI Pair:與 AI 合作改善

建議提示詞:

請使用 Python 的 @property 改善 Product 類別,讓庫存不能小於零,並提供購買商品的方法。請比較公開屬性與 property 的優缺點,程式保持適合大學生理解。

拿到 AI 程式後,請測試:負數庫存、庫存不足、購買零件商品及正常購買。

Challenge AI:需求突然改變

新規則:「庫存不允許由外部直接設定,只能透過進貨 restock() 與銷售 purchase() 改變。」請設計唯讀的 stock 屬性,並判斷這樣是否比公開 Setter 更符合需求。

九、物件導向四個重要觀念比較

觀念核心問題智慧養殖範例
Class 類別如何組織資料與行為?建立 Pond
Inheritance 繼承如何共用與擴充既有設計?SmartPond(Pond)
Polymorphism 多型如何用相同方式操作不同物件?check_status()
Encapsulation 封裝如何管理資料存取並維持合理狀態?oxygen 的驗證

十、結語:AI 會寫封裝,但誰來決定規則?

AI 可以快速產生 @property、Getter 與 Setter,但只有理解應用情境的人,才能判斷哪些資料不合理、哪些操作應被允許,以及哪些規則需要由物件自己維護。

AI 協助寫程式;學生負責理解問題、設計規則、驗證行為。這是 AI 時代學習 Python 物件導向的重要能力。

Python 物件導向教學系列:(一)Class (二)Inheritance (三)Polymorphism (四)Encapsulation
下一篇建議:整合四大觀念,設計智慧養殖監控系統。

Python 多型:為什麼相同的方法可以有不同的行為?

Python 多型(Polymorphism):為什麼相同的方法可以有不同的行為?

Python 物件導向程式設計系列(三)|Class × Inheritance × Polymorphism × AI 協作學習

Python 實習物件導向智慧養殖AI 協作

前面我們學過類別(Class)與繼承(Inheritance)。類別幫助我們組織資料與行為;繼承讓不同類別共用既有功能。接下來要學習另一個重要觀念:多型(Polymorphism)。

核心觀念:相同的方法名稱,面對不同物件,可以產生不同的行為。重點不是把程式寫得更花俏,而是讓程式更容易擴充。

一、從生活情境理解多型

如果你對學生與老師都說「請介紹自己」,學生可能說「我正在學 Python」,老師可能說「我教授 Python」。相同的指令,因對象不同而產生不同的回應。

class Student:
    def introduce(self):
        print("我是學生,正在學習 Python")

class Teacher:
    def introduce(self):
        print("我是老師,負責教授 Python")

student = Student()
teacher = Teacher()
student.introduce()
teacher.introduce()
我是學生,正在學習 Python
我是老師,負責教授 Python

兩個類別都具有 introduce() 方法,但實際執行內容不同。這也說明:Python 的多型不一定需要繼承。

二、水井村智慧養殖池:同一個方法,不同行為

假設有三種養殖池:一般養殖池顯示溶氧;智慧養殖池判斷異常;AI 智慧養殖池提供管理建議。它們都可以使用 check_status(),但執行內容不同。

check_status()相同方法呼叫Pond一般養殖池顯示溶氧數值SmartPond智慧養殖池判斷溶氧是否異常AIPondAI 智慧養殖池提供管理建議呼叫方式相同,實際行為由物件決定

三、建立父類別 Pond

class Pond:
    def __init__(self, name, oxygen):
        self.name = name
        self.oxygen = oxygen

    def check_status(self):
        return f"{self.name}:溶氧 {self.oxygen}"

父類別提供所有養殖池共通的資料與方法。

四、使用繼承與方法覆寫實現多型

class SmartPond(Pond):
    def check_status(self):
        if self.oxygen < 5:
            return f"{self.name}:溶氧偏低"
        return f"{self.name}:溶氧正常"

class AIPond(Pond):
    def check_status(self):
        if self.oxygen < 5:
            return f"{self.name}:建議檢查增氧設備"
        return f"{self.name}:目前環境正常"

子類別重新定義父類別原有的 check_status(),稱為方法覆寫(Method Overriding)。注意:此處的 AIPond 只是規則式建議,尚未連接真正的 AI 模型。

五、同一個 for 迴圈管理不同物件

ponds = [
    Pond("一號池", 5.8),
    SmartPond("二號池", 4.2),
    AIPond("三號池", 4.3)
]

for pond in ponds:
    print(pond.check_status())
一號池:溶氧 5.8
二號池:溶氧偏低
三號池:建議檢查增氧設備

這段程式不需要判斷每個物件是哪個類別。Python 會根據物件的實際類型,選擇對應的方法。這種行為也稱為動態派送(Dynamic Dispatch)。

六、新增魚菜共生養殖池,原本迴圈不必修改

class AquaponicsPond(Pond):
    def __init__(self, name, oxygen, water_level):
        super().__init__(name, oxygen)
        self.water_level = water_level

    def check_status(self):
        if self.water_level < 30:
            return f"{self.name}:水位偏低"
        if self.oxygen < 5:
            return f"{self.name}:溶氧偏低"
        return f"{self.name}:系統正常"

ponds.append(AquaponicsPond("四號池", 5.6, 25))

for pond in ponds:
    print(pond.check_status())
一號池:溶氧 5.8
二號池:溶氧偏低
三號池:建議檢查增氧設備
四號池:水位偏低

新增類別後,原本的迴圈仍然可以使用,這是多型對軟體擴充的重要價值。以上警戒值僅供教學,實際系統須依養殖物種與設備設定。

七、多型一定要有繼承嗎?

不一定。Python 有鴨子型別(Duck Typing)的特性:只要物件提供程式需要的方法,就能以相同方式使用。

class Dog:
    def speak(self):
        return "汪汪"

class Cat:
    def speak(self):
        return "喵喵"

animals = [Dog(), Cat()]
for animal in animals:
    print(animal.speak())
汪汪
喵喵

Dog 和 Cat 沒有共同的自訂父類別,卻都能回應 speak()。

八、Class、Inheritance、Polymorphism 比較

觀念核心問題養殖池範例
Class 類別如何組織資料與行為?建立 Pond
Inheritance 繼承如何共用並擴充既有設計?SmartPond(Pond)
Polymorphism 多型如何以相同方式操作不同物件?check_status()

九、AI 時代的多型實習挑戰

No-AI:先預測

不使用 AI,閱讀三種養殖池程式,預測執行結果,並說明為何相同的 check_status() 會有不同輸出。

AI Pair:比較設計

請 AI 寫出兩種版本:一種以 if/elif 判斷類別,一種使用多型。比較新增養殖池時需要修改哪些程式。

Challenge AI:擴充系統

新增 ShrimpPond 類別,提供專屬狀態檢查功能,卻不能修改原本的 for 迴圈。

建議提示詞:

我正在學習 Python 多型。請以智慧養殖系統為例,建立 Pond、SmartPond、AIPond 與 ShrimpPond 類別。每個類別都有 check_status() 方法,但執行不同功能。請展示如何用同一個 for 迴圈操作不同物件,並解釋方法覆寫、動態派送與多型的關係。程式請保持簡單,適合大學生實習。

十、結語:AI 會寫多型,但你知道何時需要嗎?

當系統只有一種養殖池時,可能不需要多型;當系統逐漸擴充,包含不同設備與監控規則時,多型就能讓程式用一致的方式操作不同物件。

最重要的觀念:相同的操作介面,可以對不同物件產生不同的行為。AI 可以幫忙寫程式,但學生仍要能判斷如何設計、何時擴充,以及是否過度設計。

AI 會寫 Python 繼承,但你知道為什麼需要繼承嗎?

AI 會寫 Python 繼承,但你知道為什麼需要繼承嗎?

Python 程式實習|物件導向程式設計(OOP)× 生成式 AI × 智慧養殖

學習目標:透過水井村智慧養殖池,理解繼承(Inheritance)、父類別、子類別、方法覆寫(Override)與 super()。最重要的不是背語法,而是能判斷「什麼時候該繼承,什麼時候不該繼承」。

一、AI 都會寫 Class,為什麼還要學繼承?

現在只要向生成式 AI 說:「幫我用 Python 設計智慧養殖池系統」,AI 就可能產生好幾個類別,甚至寫出複雜的繼承架構。但是,類別越多、繼承越深,程式就越好嗎?不一定。

繼承的核心用意,是把共同的資料與行為放在父類別,讓子類別重用並擴充功能。它不是為了炫耀語法,也不是所有程式都必須使用。

二、從三種養殖池理解繼承

假設水井村有三種養殖池:一般養殖池只需記錄資料;智慧養殖池增加溶氧警戒;AI 智慧養殖池增加管理建議。三者都有名稱與溶氧數值,因此可以共用基本設計。

圖:Pond 是父類別;SmartPond 和 AIPond 都直接繼承 Pond。這是示範架構,不表示 AI 功能一定要透過繼承實作。

三、Step 1:建立基本養殖池 Pond

class Pond:
    def __init__(self, name, oxygen):
        self.name = name
        self.oxygen = oxygen

    def show_info(self):
        print(self.name, "溶氧:", self.oxygen)

Pond 是父類別,負責共同的資料與功能。__init__() 初始化每個物件,self 代表目前操作的物件。

四、Step 2:SmartPond 繼承 Pond

class SmartPond(Pond):
    def check_oxygen(self):
        if self.oxygen < 5:
            return "溶氧偏低"
        return "溶氧正常"

pond1 = SmartPond("水井一號池", 4.2)
pond1.show_info()
print(pond1.check_oxygen())

執行結果:

水井一號池 溶氧: 4.2
溶氧偏低

注意:我們沒有在 SmartPond 裡重新寫 __init__() 和 show_info(),卻仍然可以使用它們。這是因為子類別繼承了父類別的功能。

五、Step 3:方法覆寫與 super()

如果 AI 智慧養殖池想改變顯示方式,可以在子類別重新定義同名方法,稱為方法覆寫(Override)。

class AIPond(Pond):
    def show_info(self):
        print("AI 智慧養殖監控")
        super().show_info()

    def give_advice(self):
        if self.oxygen < 5:
            return "建議檢查增氧設備"
        return "目前溶氧狀態正常"

pond2 = AIPond("水井二號池", 4.3)
pond2.show_info()
print(pond2.give_advice())

執行結果:

AI 智慧養殖監控
水井二號池 溶氧: 4.3
建議檢查增氧設備

super().show_info() 會呼叫父類別的顯示方法,避免重複撰寫原有功能。此處的「AI 建議」只是規則判斷示例,沒有真正呼叫 AI 模型;溶氧警戒值也僅供教學,實務上須依養殖條件設定。

六、繼承有什麼好處?

比較項目不使用繼承使用繼承
共同屬性可能重複定義集中在父類別
共同方法可能重複撰寫子類別可直接使用
特殊功能各類別自行設計子類別可擴充
共同功能修改可能需修改多處可集中修改父類別
複雜度小型系統直覺階層過深時難維護

七、不是所有關係都應該繼承:is-a 與 has-a

適合繼承(is-a,是一種):智慧養殖池是一種養殖池,所以 SmartPond(Pond) 是合理的示例。

適合組合(has-a,擁有):養殖池擁有感測器,但養殖池不是感測器。因此,通常應該讓養殖池包含感測器物件,而不是繼承感測器。

class Sensor:
    def __init__(self, value):
        self.value = value

class PondWithSensor:
    def __init__(self, name, sensor):
        self.name = name
        self.sensor = sensor

sensor = Sensor(5.8)
pond = PondWithSensor("水井三號池", sensor)
print(pond.name, pond.sensor.value)

這稱為組合(Composition)。判斷類別關係,比記住繼承語法更重要。

八、AI 協作實習:No-AI → AI Pair → Challenge AI

任務 A|No-AI:先自己設計

請畫出 Pond、SmartPond、AIPond 的關係,並說明哪些屬性與方法應該共用。先不要讓 AI 代答。

任務 B|AI Pair:請 AI 比較兩種版本

提示詞:

請以 Python 智慧養殖池系統為例,分別寫出「不使用繼承」與「使用繼承」的版本。兩者需有相同功能,並比較程式重複程度、可讀性、修改成本與擴充性。請保持範例簡單,適合大學生學習。

任務 C|Challenge AI:需求突然改變

新增魚菜共生系統、溫度感測器與 MQTT 資料傳輸。請先判斷:哪些適合繼承?哪些應使用組合?再讓 AI 提供設計方案,並檢查是否過度設計。

九、課後思考:你真的理解繼承了嗎?

  1. 為什麼 SmartPond 沒有寫 show_info(),仍能呼叫它?
  2. super() 在覆寫方法時有什麼用途?
  3. 「養殖池有一個感測器」為什麼不適合使用繼承?
  4. 如果 AI 幫你設計了五層繼承架構,你會如何判斷是否必要?

十、結語:AI 會寫繼承,但你要會判斷

繼承的價值不是讓程式看起來更厲害,而是重用共同設計、擴充特殊功能,並在適當情況下讓系統更容易維護。當類別之間沒有合理的「是一種」關係時,組合或簡單函式可能更合適。

AI 可以幫你寫出繼承程式,但你必須知道為什麼要繼承,以及什麼時候不應該繼承。

延伸學習:Python、Class、Object、Inheritance、Override、super()、Composition、OOP、AI Pair Programming、AIoT