顯示具有 Python 標籤的文章。 顯示所有文章
顯示具有 Python 標籤的文章。 顯示所有文章

2026年11月10日 星期二

Python 物件導向實戰:從智慧養殖認識屬性命名、名稱改寫與封裝

一、為什麼 Python 需要屬性封裝?

在前一篇文章中,我們認識了 Python 物件導向程式設計(OOP)的封裝(Encapsulation),以及三種常見的屬性命名方式:

  • self.oxygen:公開屬性
  • self._oxygen:非公開慣例
  • self.__oxygen:名稱改寫

但為什麼需要這三種寫法?它們真的能保護資料嗎?

我們從智慧養殖池的溶氧監控系統開始。

生活情境:養殖池的溶氧數值

假設我們正在開發一套智慧養殖系統,每個養殖池都有一個溶氧值(DO,Dissolved Oxygen),單位是 mg/L。

溶氧值通常不應該是負數。然而,如果我們直接使用公開屬性,外部程式就可能寫入不合理的資料。

class FishPond:
    def __init__(self, oxygen):
        self.oxygen = oxygen


pond = FishPond(6.5)

print(pond.oxygen)

pond.oxygen = -10

print(pond.oxygen)

執行結果:

6.5
-10

程式可以正常執行,但溶氧值卻變成 -10 mg/L。

問題不在 Python 程式發生錯誤,而是程式允許外部直接寫入不合理的數值。

這就是封裝希望解決的問題之一。

二、第一種寫法:公開屬性 self.oxygen

公開屬性(Public Attribute)是 Python 最直接的屬性寫法。

class FishPond:
    def __init__(self, oxygen):
        self.oxygen = oxygen


pond = FishPond(6.5)

print(pond.oxygen)

pond.oxygen = 7.2

print(pond.oxygen)

執行結果:

6.5
7.2

這種寫法非常簡單,外部可以直接讀取,也可以直接修改。

適合情境:當屬性沒有特殊的驗證需求,而且允許使用者自由修改時,公開屬性通常是最簡單、最符合 Python 風格的選擇。

但如果某個屬性具有特定規則,例如溶氧不能小於零,就需要進一步考慮如何控制修改行為。

三、第二種寫法:單底線 self._oxygen

在 Python 中,屬性名稱前面加上一個底線,通常表示:

「這個屬性主要供類別內部使用,外部程式不應隨意依賴或修改。」

class FishPond:
    def __init__(self, oxygen):
        self._oxygen = oxygen

    def show_oxygen(self):
        print("目前溶氧:", self._oxygen)


pond = FishPond(6.5)

pond.show_oxygen()

pond._oxygen = -10

pond.show_oxygen()

執行結果:

目前溶氧: 6.5
目前溶氧: -10

我們發現,雖然使用了單底線,外部仍然可以修改這個屬性。

原因是:單底線是一種命名慣例,不是 Python 強制執行的存取限制。

在某些物件導向語言中,會使用 protected 描述受保護的成員。但 Python 的單底線並不等同於 Java、C++ 等語言的存取控制機制。

可以把單底線想像成養殖池控制箱上的標籤:

內部設定,請勿任意修改。

這個標籤提醒使用者遵守規範,但並沒有真的把控制箱鎖起來。

四、第三種寫法:雙底線 self.__oxygen

如果把屬性名稱改成雙底線開頭,會發生什麼事情?

class FishPond:
    def __init__(self, oxygen):
        self.__oxygen = oxygen

    def show_oxygen(self):
        print("目前溶氧:", self.__oxygen)


pond = FishPond(6.5)

pond.show_oxygen()

print(pond.__oxygen)

執行結果:

目前溶氧: 6.5

AttributeError: 'FishPond' object
has no attribute '__oxygen'

為什麼類別內部可以使用 self.__oxygen,但是外部卻不能直接使用 pond.__oxygen?

這是因為 Python 有一項特殊機制:

名稱改寫(Name Mangling)。

五、深入理解 Name Mangling 名稱改寫

當 Python 在類別定義中遇到雙底線開頭,而且不是雙底線結尾的屬性名稱時,會依照類別名稱進行名稱改寫。

例如:

class FishPond:
    def __init__(self, oxygen):
        self.__oxygen = oxygen

Python 會將類別中的 __oxygen 改寫為:

__oxygen

↓

_FishPond__oxygen

注意:這不是加密,也不是把資料變成無法存取。

我們可以使用 __dict__ 觀察一般實例的屬性儲存內容。

class FishPond:
    def __init__(self, oxygen):
        self.__oxygen = oxygen


pond = FishPond(6.5)

print(pond.__dict__)

執行結果:

{'_FishPond__oxygen': 6.5}

現在我們知道,屬性其實仍然存在,只是名稱被改寫了。

名稱改寫真的不能從外部存取嗎?

答案是:仍然可以。

pond = FishPond(6.5)

print(pond._FishPond__oxygen)

pond._FishPond__oxygen = 8.0

print(pond._FishPond__oxygen)

執行結果:

6.5
8.0

因此,雙底線不是安全機制,也不能用來防止惡意修改資料。

它的主要目的,是避免意外的名稱衝突,特別是在繼承的情境中。

六、為什麼名稱改寫可以避免繼承時的衝突?

假設我們有一個基本養殖池類別 FishPond,以及一個智慧養殖池類別 SmartFishPond。

父類別與子類別都使用 __oxygen,會不會互相覆蓋?

class FishPond:
    def __init__(self):
        self.__oxygen = 6.5

    def show_parent(self):
        print("父類別溶氧:", self.__oxygen)


class SmartFishPond(FishPond):
    def __init__(self):
        super().__init__()
        self.__oxygen = 8.0

    def show_child(self):
        print("子類別溶氧:", self.__oxygen)


pond = SmartFishPond()

pond.show_parent()
pond.show_child()

print(pond.__dict__)

執行結果:

父類別溶氧: 6.5
子類別溶氧: 8.0
{'_FishPond__oxygen': 6.5,
 '_SmartFishPond__oxygen': 8.0}

雖然父類別與子類別都使用 __oxygen,但 Python 會根據各自定義所在的類別進行名稱改寫。

類別 改寫後的名稱
FishPond _FishPond__oxygen
SmartFishPond _SmartFishPond__oxygen

因此兩個屬性可以同時存在。

不過,這個範例只是示範名稱改寫。實際開發智慧養殖系統時,如果父類別和子類別需要共用同一個溶氧數值,通常應設計共同的屬性介面,而不是各自儲存一份不同的溶氧資料。

七、真正重要的封裝:控制資料如何被修改

到目前為止,我們發現三種屬性命名方式,都不能真正阻止外部程式修改資料。

那麼,封裝到底有什麼價值?

封裝的重點不只是隱藏資料,而是:

將資料與操作資料的規則放在同一個類別中,提供一致、可維護的操作介面。

例如,智慧養殖池的溶氧值必須符合以下條件:

  • 必須是數值。
  • 不能小於 0 mg/L。
  • 可以透過類別提供的方法更新。

先使用一般方法實作。

class FishPond:
    def __init__(self, oxygen):
        self.__oxygen = 0
        self.set_oxygen(oxygen)

    def set_oxygen(self, value):
        if isinstance(value, bool) or not isinstance(
            value, (int, float)
        ):
            print("錯誤:溶氧必須是數值")
        elif not 0 <= value < float("inf"):
            print("錯誤:溶氧必須是有限的非負數")
        else:
            self.__oxygen = value

    def get_oxygen(self):
        return self.__oxygen


pond = FishPond(6.5)

print(pond.get_oxygen())

pond.set_oxygen(-10)

print(pond.get_oxygen())

執行結果:

6.5
錯誤:溶氧必須是有限的非負數
6.5

當外部程式嘗試設定 -10 時,類別會拒絕這個不合理的數值,並保留原本的 6.5。

這就是封裝帶來的好處:由類別統一管理資料修改規則。

但要注意,這個規則只會在使用者透過 set_oxygen() 修改資料時發揮作用。它不是防止所有外部修改的安全邊界。

八、Python 更推薦的方式:@property

在其他物件導向語言中,我們經常看到 Getter 和 Setter 方法。

Python 也可以使用這種方式,但還提供了更自然的設計:

@property 裝飾器。

它讓我們可以使用類似公開屬性的寫法,同時執行讀取與設定時的控制邏輯。

class FishPond:
    def __init__(self, oxygen):
        self._oxygen = 0
        self.oxygen = oxygen

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

    @oxygen.setter
    def oxygen(self, value):
        if isinstance(value, bool) or not isinstance(
            value, (int, float)
        ):
            raise TypeError("溶氧必須是數值")

        if not 0 <= value < float("inf"):
            raise ValueError("溶氧必須是有限的非負數")

        self._oxygen = value


pond = FishPond(6.5)

print(pond.oxygen)

pond.oxygen = 7.2

print(pond.oxygen)

執行結果:

6.5
7.2

這裡有兩個重要的裝飾器:

裝飾器 功能
@property 定義讀取屬性時執行的方法
@oxygen.setter 定義設定屬性時執行的方法

例如:

print(pond.oxygen)

會透過 Getter 讀取數值。

pond.oxygen = 7.2

會透過 Setter 檢查並更新數值。

使用者不需要直接呼叫 get_oxygen() 或 set_oxygen()。

另外,這裡使用 raise 主動拋出例外,當輸入資料不符合規則時,程式不會默默接受錯誤數值。

若要讓程式在錯誤輸入後繼續執行,可以在呼叫端搭配 try...except 處理。

九、整合實作:智慧養殖池溶氧監控系統

現在我們把屬性封裝與智慧養殖的實際需求結合。

這個範例具備以下功能:

  • 建立具有名稱的養殖池。
  • 設定與讀取溶氧數值。
  • 檢查溶氧是否合理。
  • 根據溶氧數值顯示監控狀態。

以下設定 5.0 mg/L 為教學示範警戒值,實際養殖時應依魚種、環境及養殖管理需求調整。

class SmartFishPond:
    def __init__(self, name, oxygen):
        self.name = name
        self.__oxygen = 0
        self.oxygen = oxygen

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

    @oxygen.setter
    def oxygen(self, value):
        if isinstance(value, bool) or not isinstance(
            value, (int, float)
        ):
            raise TypeError("溶氧必須是數值")

        if not 0 <= value < float("inf"):
            raise ValueError("溶氧必須是有限的非負數")

        self.__oxygen = value

    def check_status(self):
        if self.oxygen < 5.0:
            return "溶氧偏低,請檢查設備"
        else:
            return "溶氧達到示範標準"

    def show_status(self):
        print("養殖池:", self.name)
        print("溶氧:", self.oxygen, "mg/L")
        print("狀態:", self.check_status())


pond = SmartFishPond("水井村一號池", 6.5)

pond.show_status()

print("----- 更新感測數據 -----")

pond.oxygen = 3.8

pond.show_status()

執行結果:

養殖池: 水井村一號池
溶氧: 6.5 mg/L
狀態: 溶氧達到示範標準
----- 更新感測數據 -----
養殖池: 水井村一號池
溶氧: 3.8 mg/L
狀態: 溶氧偏低,請檢查設備

在這個類別中,我們同時使用了三種概念:

  • self.name:公開養殖池名稱。
  • self.__oxygen:以名稱改寫降低意外名稱衝突的風險。
  • @property:提供一致的溶氧讀取與更新介面,並驗證數值。

這個範例仍屬教學用程式,並未直接連接感測器或控制增氧設備。未來可以結合 ESP32、Raspberry Pi Pico W、MQTT 或雲端監控平台,擴充為實際的智慧養殖應用。

十、三種屬性命名與 @property 比較

寫法 主要用途 是否阻止外部存取
self.oxygen 公開屬性 否
self._oxygen 內部使用慣例 否
self.__oxygen 名稱改寫、降低命名衝突 不允許以原名稱直接存取,但仍可用改寫後名稱存取
@property 控制屬性讀取與設定行為 可控制正常介面的讀寫,但不是安全隔離機制

十一、課堂思考與延伸練習

練習一:屬性命名

請將智慧養殖池中的 oxygen 分別改為 _oxygen 與 __oxygen,觀察外部程式存取時的差異。

練習二:名稱改寫

使用 __dict__ 印出物件的屬性,觀察 Python 如何改寫雙底線屬性名稱。

練習三:資料驗證

修改 @property 的 Setter,讓溶氧值必須介於 0~20 mg/L。這是程式驗證練習範圍,不代表所有養殖環境的實際量測範圍。

練習四:智慧養殖擴充

新增 temperature 水溫屬性,並使用 @property 驗證輸入是否為合理數值。

進階挑戰:設計一個 SmartFishPond 類別,同時管理溶氧、水溫與水位,並利用方法輸出完整的養殖池狀態。

十二、結語:封裝不只是把資料藏起來

學習 Python 物件導向程式設計時,很容易誤以為封裝就是把屬性改成雙底線。

但真正重要的觀念是:

公開屬性,讓資料容易使用。

單底線,提醒哪些資料屬於內部實作。

雙底線,降低繼承時意外命名衝突的風險。

@property,讓資料操作遵守類別定義的規則。

因此,Python 的封裝不是單純追求「不讓別人看到資料」,而是透過良好的類別設計,讓資料與操作行為保持一致。

從智慧養殖池的溶氧監控開始,我們可以進一步整合繼承、多型、例外處理與物聯網感測技術,逐步建立完整的智慧生活應用系統。

延伸閱讀:
Python 物件導向程式設計:屬性命名與封裝(前篇)

2026年11月4日 星期三

Python、Java、C++ 物件導向比較:智慧養殖池實作

Python 程式實習 · OOP 跨語言實戰

為什麼 Python、Java、C++ 都支援物件導向?

從同一個智慧養殖池範例,比較三種程式語言的 OOP 設計

當生成式 AI 能在幾秒鐘內將 Python 程式轉換成 Java 或 C++,大學生還需要學習物件導向嗎?答案是需要。因為真正值得學習的,不是記住三種語言的語法,而是理解資料如何組織、功能如何分工、系統如何擴充。

一、從智慧養殖池提出共同需求

假設要管理兩座養殖池:一般池與智慧池。每座池子有名稱與溶氧值,能回報狀態;智慧池另外在溶氧低於示範門檻時加上警示文字。三種語言都使用同一套需求,才方便比較。

教學提醒:本文的 5 mg/L 僅用於示範條件判斷,不是所有魚種或養殖環境通用的操作標準;真實監控須依物種、場域及感測器規格設定。

二、先理解共同的 OOP 架構

我們設計基底類別 Pond,再設計繼承它的 SmartPond。兩者都能呼叫 status(),但智慧池會覆寫這個方法。

Pondstatus()SmartPond覆寫 status()同一個 pond.status() 呼叫 → 依實際物件執行不同版本

這個架構展現三個核心觀念:類別組織資料與行為、繼承重用共同設計、多型讓相同呼叫依物件類型產生不同結果。封裝與抽象則進一步幫助我們管理資料規則和公開介面。

三、Python:用簡潔語法表達設計

Python 使用 class 宣告類別,__init__ 初始化資料,super() 呼叫父類別方法。方法覆寫不需要特別宣告關鍵字。

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

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

class SmartPond(Pond):
    def status(self):
        base = super().status()
        return base + ("(示範警示)" if self.oxygen < 5 else "(正常)")

ponds = [Pond("一般池", 6.2), SmartPond("智慧池", 4.3)]
for pond in ponds:
    print(pond.status())

四、Java:用明確的型別與繼承表達設計

Java 使用 extends 繼承,@Override 清楚標示覆寫方法。範例存成 Main.java,可使用 javac Main.java 編譯,再以 java Main 執行。

class Pond {
    protected String name;
    protected double oxygen;

    Pond(String name, double oxygen) {
        this.name = name;
        this.oxygen = oxygen;
    }

    String status() {
        return String.format("%s:溶氧 %.1f mg/L", name, oxygen);
    }
}

class SmartPond extends Pond {
    SmartPond(String name, double oxygen) {
        super(name, oxygen);
    }

    @Override
    String status() {
        return super.status() +
            (oxygen < 5 ? "(示範警示)" : "(正常)");
    }
}

public class Main {
    public static void main(String[] args) {
        Pond[] ponds = {
            new Pond("一般池", 6.2),
            new SmartPond("智慧池", 4.3)
        };
        for (Pond pond : ponds) {
            System.out.println(pond.status());
        }
    }
}

五、C++:以 virtual 支援執行階段多型

C++ 透過 public Pond 繼承,並用 virtual 與 override 實現執行階段多型。範例使用 unique_ptr 管理物件生命週期,避免手動 delete。需使用 C++14 或更新版本,例如 g++ -std=c++14 main.cpp -o main。

#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <sstream>
#include <iomanip>
using namespace std;

class Pond {
protected:
    string name;
    double oxygen;
public:
    Pond(string name, double oxygen)
        : name(name), oxygen(oxygen) {}
    virtual ~Pond() = default;

    virtual string status() const {
        ostringstream out;
        out << name << ":溶氧 " << fixed << setprecision(1)
            << oxygen << " mg/L";
        return out.str();
    }
};

class SmartPond : public Pond {
public:
    using Pond::Pond;
    string status() const override {
        return Pond::status() +
            (oxygen < 5 ? "(示範警示)" : "(正常)");
    }
};

int main() {
    vector<unique_ptr<Pond>> ponds;
    ponds.push_back(make_unique<Pond>("一般池", 6.2));
    ponds.push_back(make_unique<SmartPond>("智慧池", 4.3));
    for (const auto& pond : ponds) {
        cout << pond->status() << '\n';
    }
}

六、三種語言執行結果

上述三個程式的輸出概念相同:

一般池:溶氧 6.2 mg/L
智慧池:溶氧 4.3 mg/L(示範警示)

即使語法差異很大,背後的設計仍然一致:先定義共同介面,再允許不同物件提供自己的實作。

七、重要語法比較

概念PythonJavaC++
定義類別class Pond:class Pond { }class Pond { };
建構與初始化__init__建構子 Pond(...)建構子及初始化列表
繼承SmartPond(Pond)extends Pond: public Pond
呼叫父類別super()superPond::
覆寫方法同名方法@Overridevirtual / override
多型容器物件串列Pond[]vector<unique_ptr<Pond>>
記憶體管理自動管理為主垃圾回收為主RAII/智慧指標

八、封裝與抽象:三種語言有何不同?

本文範例主要聚焦類別、繼承與多型。若要強化封裝,可進一步限制溶氧資料的修改方式。Python 常用底線慣例與 @property;Java 可用 private 搭配方法;C++ 可用 private 或 protected 搭配成員函式。三種語言也都能透過公開方法或抽象介面,讓使用者不必了解內部實作細節。

重要觀念:Python 的 _oxygen 是命名慣例,不等同於 Java 或 C++ 的語言層級 private 存取限制。

九、AI 時代為什麼仍要學跨語言 OOP?

AI 可以快速翻譯語法,但不能代替學生確認設計是否符合需求。例如:智慧池真的應該繼承一般池嗎?溶氧值由誰負責驗證?如果加入魚菜共生池,現有的監控迴圈需不需要修改?這些都是架構設計問題,而非單純的語法轉換。

十、No-AI → AI Pair → Challenge AI 課堂實習

階段一|No-AI:觀察與比較

  1. 找出三種語言的類別、建構子、繼承與覆寫位置。
  2. 預測兩座池子的輸出,並說明多型如何發生。
  3. 將智慧池的溶氧改成 5.8,觀察結果。

階段二|AI Pair:合作轉換與檢查

請 AI 將 Python 版本改寫成 Java 或 C++,但要求保留原有類別架構。學生需逐項檢查建構子、覆寫、輸出及 C++ 記憶體管理,而不是只確認程式可以執行。

階段三|Challenge AI:新增魚菜共生池

新增 AquaponicsPond,除了溶氧還有水位;要求沿用同一個 status() 介面。學生先畫類別圖,再請 AI 協助撰寫三種語言版本,最後設計測試案例驗證。

十一、課後反思

  1. 如果未來改用 C# 或 Kotlin,哪些 OOP 觀念仍然適用?
  2. 為什麼 C++ 需要注意 virtual 與物件生命週期?
  3. 如果養殖池監控加入 MQTT 感測器,感測器應該繼承 Pond,還是讓 Pond 持有感測器物件?為什麼?

十二、結語:語法會改變,設計能力可以帶著走

Python、Java、C++ 的寫法不同,但它們都能用類別與物件組織系統,也能透過繼承與多型降低重複設計。當 AI 能替我們轉換程式語言時,理解問題、規劃責任、選擇合適的物件關係,以及驗證程式行為,才是大學生應該建立的核心能力。

教學定位:Python 程式實習/物件導向整合與跨語言比較。本文程式為可獨立執行的示範版本,未連接真實感測器。

目前有哪些程式語言支援物件導向程式設計?

PYTHON 程式實習・物件導向延伸閱讀

目前有哪些程式語言支援物件導向程式設計?

從 Python、Java、C++ 到 JavaScript、Go、Rust:比較不同語言如何組織資料與行為

學會 Python 的類別(Class)、繼承(Inheritance)、多型(Polymorphism)與封裝(Encapsulation)之後,學生可能會問:只有 Python 能寫物件導向程式嗎?換成 Java、C++ 或 JavaScript,還能使用相同的設計觀念嗎?

答案是:目前多數主流程式語言都支援某種形式的物件導向設計。不過,各語言的物件模型不完全相同,也不是每種語言都提供傳統的類別繼承。

本篇學習目標:認識常見 OOP 語言、比較類別式與原型式物件模型、理解「沒有類別繼承」不等於「不能做物件導向設計」,並透過智慧養殖池範例跨語言比較。

一、什麼是物件導向程式設計(OOP)?

物件導向程式設計(Object-Oriented Programming)是一種軟體設計方式,強調把相關的資料(狀態)與操作(行為)組織成物件,並透過明確的介面進行互動。

例如,一座智慧養殖池可以有名稱、溶氧濃度、水溫等資料,也可以提供顯示狀態、更新感測值、判斷是否警示等操作。

Pond 智慧養殖池資料(狀態)name、oxygen、temperature方法(行為)show()、check_status()感測器 / 使用者監控畫面 / 警示

圖一:將養殖池的資料與操作集中在物件內,是理解 OOP 的第一步。

二、常見支援物件導向的程式語言

程式語言OOP 主要方式常見應用
Python類別式,支援多重繼承AI、資料分析、IoT、網站
Java類別式,單一類別繼承、介面企業系統、Android
C++類別式,支援多重繼承系統、遊戲、嵌入式
C#類別式,單一類別繼承、介面.NET、Unity、企業系統
JavaScript原型式,提供 class 語法網頁、Node.js
TypeScriptJavaScript 物件模型+型別系統大型前後端應用
PHP類別式,介面與 Trait網站後端
Ruby類別式,Mixin網站與自動化
Swift類別、結構與協定iOS、macOS
Kotlin類別式,介面Android、後端
Dart類別式,MixinFlutter App
Scala物件導向與函數式混合JVM、大數據
GoStruct、方法、介面;無類別繼承雲端、網路服務
RustStruct、impl、Trait;無類別繼承系統、嵌入式
Visual Basic .NET類別式.NET 應用

其他具有物件導向能力的語言還包括 Smalltalk、Eiffel、Delphi(Object Pascal)、Objective-C 等。重點不在背誦語言名單,而是辨識每種語言使用什麼機制來組織資料、共用行為與實現多型。

三、所有語言都有 OOP 四大觀念嗎?

課堂上常用「封裝、繼承、多型、抽象」來整理 OOP;但這四項是設計概念,不代表每種語言都必須提供相同的語法。

語言封裝傳統類別繼承多型抽象機制
Python慣例、property有有ABC、Protocol 等
Java存取修飾詞有有抽象類別、介面
C++存取修飾詞有有抽象類別等
C#存取修飾詞、屬性有有抽象類別、介面
JavaScript私有欄位等原型繼承有物件介面與慣例
Go套件可見性沒有有介面
Rust模組可見性沒有有Trait
重要提醒:JavaScript 的 class 是建立在原型機制上的語法;Go 與 Rust 沒有傳統類別繼承,但可以透過介面、Trait 與組合達到許多相似的設計目標。Python 的單底線與雙底線也不是 Java 那種嚴格的 private 存取控制。

四、同一個智慧養殖池,換不同語言怎麼寫?

以下只展示「建立物件並顯示名稱」的最小範例,方便比較語法。各語言的執行環境與完整專案設定不同。

範例一:Python
class Pond:
    def __init__(self, name):
        self.name = name

    def show(self):
        print(self.name)

pond = Pond("水井一號池")
pond.show()
範例二:Java(完整可執行範例)
class Pond {
    String name;

    Pond(String name) {
        this.name = name;
    }

    void show() {
        System.out.println(name);
    }
}

public class Main {
    public static void main(String[] args) {
        Pond pond = new Pond("水井一號池");
        pond.show();
    }
}
範例三:JavaScript
class Pond {
    constructor(name) {
        this.name = name;
    }

    show() {
        console.log(this.name);
    }
}

const pond = new Pond("水井一號池");
pond.show();
範例四:Go(Struct + Method)
package main

import "fmt"

type Pond struct {
    Name string
}

func (p Pond) Show() {
    fmt.Println(p.Name)
}

func main() {
    pond := Pond{Name: "水井一號池"}
    pond.Show()
}

這四個範例都會顯示:

水井一號池

雖然語法不同,但共同的設計思路是:讓資料與相關操作保持關聯。Python、Java 與 JavaScript 使用 class;Go 則使用 struct 搭配方法。

五、沒有類別繼承,也能做到多型嗎?

可以。以 Go 為例,若不同裝置都提供 Show() 方法,就能透過介面以一致方式操作:

package main

import "fmt"

type Displayable interface {
    Show()
}

type Pond struct { Name string }
func (p Pond) Show() { fmt.Println("養殖池:", p.Name) }

type Sensor struct { Name string }
func (s Sensor) Show() { fmt.Println("感測器:", s.Name) }

func display(item Displayable) {
    item.Show()
}

func main() {
    display(Pond{Name: "水井一號池"})
    display(Sensor{Name: "溶氧感測器"})
}

這段程式沒有使用類別繼承,但 display() 可以接受不同型別的物件,展現多型設計。這也提醒我們:學習 OOP 的核心是理解設計概念,而非只記住某種語法。

六、哪些語言不是以 OOP 為主要方式?

C

主要是程序式語言,沒有內建 class 與類別繼承,但可用 struct、函式與指標模擬部分 OOP 設計。

Haskell

主要是函數式語言,使用 Typeclass 等機制表達抽象與多型,不是傳統類別式 OOP。

SQL

主要用於資料查詢與操作,不是一般用途的物件導向程式語言。

也要避免把「不以 OOP 為主」理解成「比較落後」。不同語言與典範各有擅長的問題領域。

七、為什麼大學生適合先用 Python 學 OOP?

  1. 語法相對簡潔:能先專注於類別、物件、繼承與多型,而不是大量樣板程式碼。
  2. 容易連結生活情境:飲料店 POS、學生選課、智慧養殖池都能用同一套設計概念來分析。
  3. 方便延伸到實務:可以從 Pond、Sensor、Controller 出發,逐步加入 MQTT、Web API、資料庫與 AIoT。

當學生理解 Python 的 OOP 設計後,學習 Java、C++ 或 JavaScript 時,就可以比較「概念相同在哪裡、語法與限制不同在哪裡」。

八、AI 時代的三階段學習活動

第一階段:No-AI|先自己比較

不使用 AI,閱讀 Python 與 JavaScript 的 Pond 範例,圈出「屬性」、「建構流程」、「方法」與「建立物件」的位置,並說明兩者相似之處。

第二階段:AI Pair|請 AI 協助跨語言轉換

建議提示詞:「請把這段 Python Pond 類別改寫成 JavaScript 與 Java,保留相同功能,並逐行比較建構子、屬性、方法與物件建立方式。請提供可執行的完整範例。」

學生應實際執行、核對輸出,不應只因為 AI 產生了程式碼就認為轉換成功。

第三階段:Challenge AI|檢查設計是否合理

假設系統增加「智慧養殖池、魚菜共生池、溶氧感測器」三種物件,請學生先判斷哪些適合用繼承、哪些適合用組合,再請 AI 提出架構,最後比較其合理性。

思考問題:感測器是養殖池的一種(is-a),還是養殖池擁有的設備(has-a)?如果是後者,通常應優先考慮組合,而非硬套繼承。

九、課後練習

  1. 列出三種支援類別式 OOP 的語言,說明各自常見的應用。
  2. 為什麼 JavaScript 雖然有 class,仍常被稱為原型式語言?
  3. Go 沒有傳統類別繼承,為什麼仍可以實現多型?
  4. 把 Python 的 Pond 類別新增 oxygen 屬性與 check_status() 方法,再嘗試改寫為另一種語言。
  5. 請說明「物件導向是一種設計方法,而非某個語言的專利」的意思。

十、結語:真正重要的是跨語言的設計能力

物件導向不是 Python 的專利,也不等於每一個程式都必須使用 class。 學會辨識資料、行為、介面與物件間的關係,才有能力在不同程式語言之間選擇合適的設計方式。即使 AI 能快速翻譯程式碼,學生仍需要判斷轉換後的程式是否符合原語言的特性與實際需求。

延伸閱讀建議:下一篇可使用同一個智慧養殖池案例,深入比較 Python、Java 與 C++ 的類別、封裝、繼承及多型。

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年10月31日 星期六

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
下一篇建議:整合四大觀念,設計智慧養殖監控系統。

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

AI 會寫 Python Class,但你知道為什麼要用類別嗎?

Python 程式實習生成式 AIOOP 物件導向

AI 會寫 Python Class,但你知道為什麼要用類別嗎?

從水井村智慧養殖池出發,學習 Python 類別、物件、屬性與方法,並用 AI 協作挑戰自己的程式設計能力。

一、AI 都會寫程式了,為什麼還要學 Class?

現在只要對 ChatGPT 或 Gemini 說:「請用 Python 設計智慧養殖池管理系統」,AI 就可能在幾秒鐘內產生包含 class、__init__()、self 的程式。

但是,AI 能寫出程式,不代表我們知道為什麼要這樣設計。大學生需要學習的不只是語法,還包括需求分析、程式架構、驗證與維護。

這篇文章的核心問題:什麼時候使用變數或字典就夠了?什麼時候使用 Class 更有幫助?

二、只有一個養殖池,需要 Class 嗎?

假設水井村只有一座養殖池,需要記錄水溫和溶氧:

pond_name = "水井一號池"
temperature = 28.5
oxygen = 5.8

print("養殖池:", pond_name)
print("水溫:", temperature)
print("溶氧:", oxygen)

執行結果:

養殖池: 水井一號池
水溫: 28.5
溶氧: 5.8

這樣寫很簡單,而且完全不需要類別。不是每個 Python 程式都需要 Class。

三、增加到三個、甚至一百個養殖池呢?

pond1_temp = 28.5
pond1_oxygen = 5.8

pond2_temp = 29.1
pond2_oxygen = 4.2

pond3_temp = 27.8
pond3_oxygen = 6.1

當資料量增加,建立大量獨立變數會越來越難管理。你可能想到使用字典:

pond1 = {
    "name": "水井一號池",
    "temperature": 28.5,
    "oxygen": 5.8
}

pond2 = {
    "name": "水井二號池",
    "temperature": 29.1,
    "oxygen": 4.2
}

print(pond1["name"])
print(pond2["oxygen"])

字典已經讓資料更有組織。如果搭配串列與迴圈,也能管理很多養殖池。所以,為什麼還需要類別?

四、養殖池不只有資料,還有行為

養殖池除了保存水溫、溶氧,還要能判斷是否異常。我們先使用函式:

def check_oxygen(oxygen):
    if oxygen < 5:
        return "溶氧偏低"
    return "溶氧正常"

pond1 = {"name": "水井一號池", "oxygen": 5.8}
pond2 = {"name": "水井二號池", "oxygen": 4.2}

print(pond1["name"], check_oxygen(pond1["oxygen"]))
print(pond2["name"], check_oxygen(pond2["oxygen"]))

這種方式沒有錯,對小型程式也很實用。不過,如果每座養殖池都有自己的警戒值、更新方式和操作規則,就可以考慮將資料與相關行為組織在同一個物件中。

五、Class 就像設計藍圖

同一張養殖池設計藍圖,可以建立很多座不同的養殖池。每座池子擁有自己的狀態,但共用相同的操作方式。

Class:Pond
設計藍圖
屬性+方法
Object:pond1
水井一號池
溶氧 5.8
Object:pond2
水井二號池
溶氧 4.2
觀念生活化說明Python 寫法
Class 類別養殖池設計藍圖class Pond
Object 物件實際的一座養殖池pond1
Attribute 屬性水溫、溶氧、名稱self.oxygen
Method 方法檢查溶氧等操作check_oxygen()
class Pond:
    def __init__(self, name, temperature, oxygen):
        self.name = name
        self.temperature = temperature
        self.oxygen = oxygen

    def show_info(self):
        print("養殖池:", self.name)
        print("水溫:", self.temperature)
        print("溶氧:", self.oxygen)

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


pond1 = Pond("水井一號池", 28.5, 5.8)
pond2 = Pond("水井二號池", 29.1, 4.2)

pond1.show_info()
print(pond1.check_oxygen())
pond2.show_info()
print(pond2.check_oxygen())

__init__() 是建立物件時執行的初始化方法;self 指向目前正在操作的物件。self.oxygen 是屬性,check_oxygen() 是方法。

六、用一個類別管理多個養殖池

延續上一節的 Pond 類別:

ponds = [
    Pond("水井一號池", 28.5, 5.8),
    Pond("水井二號池", 29.1, 4.2),
    Pond("水井三號池", 27.8, 6.1)
]

for pond in ponds:
    print(pond.name, pond.check_oxygen())
水井一號池 溶氧正常
水井二號池 溶氧偏低
水井三號池 溶氧正常

只寫一次類別,就可以建立許多具有各自資料的物件。當共同規則改變時,也能集中修改相關方法。

七、進階:每個養殖池有不同的警戒值

現在增加需求:不同養殖池可以設定不同的溶氧警戒值,並更新感測器讀值。

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

    def check_oxygen(self):
        if self.oxygen < self.limit:
            return "溶氧偏低"
        return "溶氧正常"

    def update_oxygen(self, value):
        self.oxygen = value


pond1 = Pond("水井一號池", 5.8, 5.0)
pond2 = Pond("水井二號池", 4.2, 4.5)

print(pond1.name, pond1.check_oxygen())
print(pond2.name, pond2.check_oxygen())

pond2.update_oxygen(5.2)
print("更新後:", pond2.check_oxygen())
水井一號池 溶氧正常
水井二號池 溶氧偏低
更新後: 溶氧正常

提醒:以上數值與警戒值僅供程式教學,實際養殖系統應依物種及現場條件設定。

八、AI 時代的三階段學習法

任務一:No-AI|先自己分析

暫時不使用 AI。假設需要管理 50 個養殖池,包含名稱、水溫、溶氧與水位。你會選擇大量變數、字典與串列,還是類別?請說明理由。沒有規定一定要選 Class。

任務二:AI Pair|請 AI 提供兩種設計

請使用 Python 設計一個智慧養殖池管理系統,提供兩個版本:第一版使用字典、串列與函式,不使用 Class;第二版使用 Pond 類別。兩個版本都要管理三座養殖池、更新溶氧數值、判斷各池是否低於自己的警戒值。請讓程式容易理解,並比較優缺點。
比較項目字典與函式Class
少量資料簡單直覺可能增加結構
多個養殖池搭配串列管理建立多個物件
資料與行為通常分開組織可整合於物件
擴充與維護取決於程式結構複雜系統可能更有優勢

任務三:Challenge AI|需求改變時,誰能做決定?

現在要求每個養殖池新增水位、水溫更新方法及異常警示。先自己決定哪些是屬性、哪些是方法,再請 AI 修改程式。最後檢查它有沒有過度設計。

請檢查目前的 Pond 類別是否有重複程式、不必要的屬性或過度複雜的設計。先提出改善建議,不要直接修改程式,讓我自己判斷。

九、實作挑戰:設計自己的智慧生活類別

  • 智慧教室:建立 Classroom,管理教室溫濕度與燈光狀態。
  • 智慧農業:建立 Farm,管理土壤濕度與灌溉狀態。
  • Pico W 感測器:建立 Sensor,管理感測器編號與最新測量值。
  • 智慧商品訂購:建立 Product,管理商品價格、庫存與購買操作。

完成後請回答:「如果不用 Class,我的程式會比較簡單,還是比較複雜?為什麼?」

十、從 Class 走向 AIoT 系統

未來智慧養殖系統可能結合 Raspberry Pi、ESP32、MQTT 與雲端服務,並依實際需求設計以下類別:

  • Pond:管理養殖池狀態。
  • Sensor:管理感測器資料。
  • MQTTClient:負責 MQTT 訊息收發。
  • AlertManager:負責異常警示。
  • DataLogger:負責儲存歷史資料。

這些只是可能的架構,不是每個系統都必須建立這麼多類別。好的設計應根據需求選擇,而不是為了使用 OOP 而使用 OOP。

十一、結語:AI 會寫 Class,你要會設計 Class

學習 Python 類別,不只是記住 class、__init__()、self,而是要理解何時需要類別、如何分配屬性與方法,以及當需求改變時如何維護程式。

AI 可以幫你寫出 Class,但你必須知道為什麼需要 Class。
AI 負責加速寫程式,學生負責理解問題、設計架構與驗證結果。

課後思考題:如果 AI 在十秒內寫出包含五個類別的 Python 程式,你如何判斷這五個類別是否真的有必要?

2026年10月26日 星期一

從五篇文章看懂 Python:串起 TCP、MCP、Agent、FastAPI 與 AIoT

Python 網路程式設計系列|程式語言特性總整理

從五篇文章看懂 Python:串起 TCP、MCP、Agent、FastAPI 與 AIoT

從 asyncio TCP 的收發資料,到 FastAPI 建立 REST API、MCP Server 公布工具、Agent 選用工具,再到 AIoT 接收感測資料與控制設備,表面上看起來是五種不同技術,其實都能用同一套 Python 基礎概念來理解。這篇文章回到 Python 程式語言本身,整理函式、型別標註、裝飾器、非同步、模組、例外處理與資源管理,說明它們如何讓不同技術接在一起。

一、五篇文章,其實是一條逐步擴大的程式路徑

主題程式要處理的問題主要 Python 概念
asyncio TCP Server/Client如何在網路連線等待資料時,繼續服務其他連線?async def、await、coroutine、事件迴圈、StreamReader/StreamWriter
MCP Server/Client如何用共同協定探索工具並傳送工具參數?函式、型別標註、模組、非同步連線、序列化資料
Agent 呼叫 MCP Server如何讓 Agent 理解任務並選擇工具?物件組合、工具函式、裝飾器、非同步呼叫、例外處理
FastAPI REST API 與 MCP如何同時服務一般程式與 AI host?型別標註、裝飾器、資料模型、模組化、共用服務函式
FastAPI、MCP、Agent 與 AIoT如何把感測資料、一般 API、Agent 工具和設備控制接起來?資料結構、函式重用、非同步 I/O、事件驅動、資源生命週期

理解這些程式時,可以先分成三層:Python 語言負責表達資料與流程;框架或 SDK 負責 HTTP、MCP、TCP 等協定細節;應用程式則決定要提供什麼功能。FastAPI、MCP SDK 和 Agent SDK 不是 Python 語言本身,而是利用 Python 語言特性建構出來的工具。

二、函式是共用功能的基本單位

在 TCP、REST、MCP 和 AIoT 程式裡,函式都負責一段清楚的工作。把實際商業邏輯放進一般 Python 函式,再由不同入口呼叫,可以避免 REST 和 MCP 各自複製一套判斷規則。

def check_temperature(temperature: float) -> str:
    """依溫度回傳設備狀態建議。"""
    if temperature >= 35:
        return "溫度偏高,建議檢查通風設備"
    return "溫度在設定範圍內"

REST endpoint 可以呼叫 check_temperature() 並回傳 JSON;MCP tool 也可以呼叫相同函式,再把結果交給 Agent。這就是「入口不同、服務邏輯共用」的核心。

Python 的可組合性:函式可以被傳入其他函式、放在模組中匯入,也可以被框架註冊成路由或工具。框架接手協定工作,程式設計者仍然可以用普通函式描述領域功能。

三、型別標註讓程式意圖更清楚

例如 temperature: float 表示預期輸入是浮點數,-> str 表示預期回傳文字。型別標註讓 IDE、自動補全、靜態檢查工具和框架能了解程式介面。FastAPI 會讀取型別標註與 Pydantic 模型來驗證 HTTP 資料;MCP SDK 也會依工具函式的參數和型別建立輸入描述。

from pydantic import BaseModel, Field

class SensorReading(BaseModel):
    device_id: str = Field(min_length=1)
    temperature: float = Field(ge=-20, le=80)
    humidity: float = Field(ge=0, le=100)

def summarize(reading: SensorReading) -> str:
    return f"{reading.device_id}:{reading.temperature}°C"

SensorReading 不只說明欄位名稱,也把數值限制寫在資料模型裡。API 接收到不符合條件的資料時,FastAPI/Pydantic 可以回報驗證錯誤。要留意的是,Python 執行環境本身不會自動強制所有型別標註;真正的執行期驗證需要 Pydantic 或自行撰寫檢查。

四、裝飾器把一般函式註冊成路由或工具

裝飾器語法以 @ 開頭,常用來替函式加上框架提供的行為。函式本身仍是 Python 函式,裝飾器則告訴框架如何使用它。

# FastAPI:把函式註冊成 HTTP GET 路由
@app.get("/api/sensors/{device_id}")
async def read_sensor(device_id: str):
    return await sensor_service.latest(device_id)

# MCP SDK:把函式註冊成 Agent 可探索的工具
@mcp.tool()
def get_latest_temperature(device_id: str) -> str:
    """讀取指定設備的最新溫度。"""
    return sensor_service.temperature_text(device_id)

第一個裝飾器把函式和 HTTP 路徑關聯起來;第二個把函式及說明註冊到 MCP Server。Agent 可以透過 MCP Client 探索工具名稱與輸入欄位,再決定是否呼叫它。裝飾器讓宣告方式簡潔,但除錯時仍要知道函式何時被框架呼叫、參數由誰提供,以及錯誤會如何回傳。

五、async/await:等待 I/O 時讓出執行權

一般同步程式在等待網路回應時,執行緒通常停在該處。使用 async def 定義 coroutine,並在等待非同步 I/O 的地方使用 await,事件迴圈就能先安排其他等待中的工作。Python 官方文件說明 asyncio 用於以 async/await 撰寫並行程式,適合 I/O 密集的網路程式。

import asyncio

async def handle_client(reader, writer):
    try:
        data = await reader.readline()
        message = data.decode("utf-8").strip()
        writer.write(f"收到:{message}\n".encode("utf-8"))
        await writer.drain()
    finally:
        writer.close()
        await writer.wait_closed()

async def main():
    server = await asyncio.start_server(
        handle_client, "127.0.0.1", 8888
    )
    async with server:
        await server.serve_forever()

asyncio.run(main())
  • async def 宣告非同步函式,呼叫後會產生 coroutine。
  • await reader.readline() 表示目前工作要等待資料;等待期間事件迴圈可以處理其他連線。
  • async with 管理 server 的生命週期;finally 確保連線結束時關閉 writer。
  • asyncio.run() 建立事件迴圈並執行最上層 coroutine。
不是所有程式都要 async:asyncio 特別適合網路、檔案或資料庫等 I/O 等待很多的工作。CPU 密集運算、阻塞式函式若直接放進 async 函式,仍會卡住事件迴圈;需要改用非同步版本、執行緒或程序。citeturn21search0

六、模組與匯入讓專案各司其職

從單一檔案逐步發展成服務時,可以依責任拆成模組:TCP Server 管理連線,FastAPI 模組管理 HTTP 路由,MCP 模組註冊工具,Agent 模組負責任務流程,AIoT service 模組管理感測資料和設備規則。

project/
├── service.py       # 共用業務邏輯、資料存取
├── tcp_server.py    # asyncio TCP 入口
├── api_server.py    # FastAPI REST 入口
├── mcp_server.py    # MCP tools 入口
├── agent_client.py  # Agent/MCP Client
└── mqtt_adapter.py  # MQTT 設備訊息接收與命令發布

例如 from service import latest_reading 把共用函式匯入 API 或 MCP 模組。這種結構讓程式可以分別啟動、替換或測試;到了多程序或多台主機部署時,共用資料應放到資料庫或訊息系統,而不是期待不同程序共用 Python 記憶體。

七、資料結構與 JSON:把 Python 物件送過網路

網路協定傳送的是位元組,不是 Python 物件。常見流程是先把設備資料整理成 dict,轉成 JSON 字串,再編碼成 UTF-8 bytes。接收方則反向解碼和解析。REST、MCP 與 MQTT 都需要明確的資料格式。

import json

reading = {
    "device_id": "pond-01",
    "temperature": 28.6,
    "dissolved_oxygen": 6.4,
}

payload = json.dumps(reading, ensure_ascii=False)
wire_bytes = payload.encode("utf-8")

received_text = wire_bytes.decode("utf-8")
received_data = json.loads(received_text)
print(received_data["temperature"])

TCP 本身只提供連續的 bytes stream,不會替應用程式標記每一則訊息的邊界;文章中的 TCP 範例需要自行約定換行、固定長度或長度前綴等 framing 方式。HTTP 和 MCP 則在協定層定義了訊息如何包裝。理解「應用程式資料結構」和「網路傳輸格式」是兩個不同層次,可避免把 TCP 封包誤當成一筆完整 JSON 訊息。

八、循序圖:同一套 Python 服務,從資料到 Agent 工具

循序圖聚焦在使用端的兩種請求方式:一般程式透過 FastAPI 呼叫 REST endpoint;Agent 則透過 MCP Client 探索工具,再由 MCP Server 呼叫同一個 Python service。感測資料可由 MQTT adapter 先寫入共用儲存層。

循序圖重點不是讓一支 Python 程式包辦所有工作,而是讓不同介面透過明確的函式和資料結構合作。對一般程式來說,FastAPI 路由是明確入口;對 Agent 來說,MCP tool 是描述清楚的功能入口;底層服務仍維持可重用、可驗證。

九、Python 的資源管理與錯誤處理

網路連線、檔案、資料庫交易和伺服器工作階段都需要正確開啟和關閉。同步程式常用 with;非同步程式使用 async with。在 asyncio TCP 中,連線關閉可放入 finally;在 FastAPI 整合 MCP 時,應用程式 lifespan 可負責啟動和結束 MCP session manager。Python 的 contextlib 提供 asynccontextmanager,用來建立非同步 context manager。citeturn21search10

錯誤處理也應依邊界分層:輸入格式錯誤回傳驗證訊息;網路中斷記錄連線錯誤並重試;設備命令逾時回報未確認狀態;Agent 無法使用模型時顯示可理解的錯誤。不要把密碼、API key 或內部例外堆疊直接回傳給使用者。

十、從語言特性走到完整系統

  1. 先練函式和資料結構:用普通函式讀取、驗證和處理感測資料。
  2. 再練 TCP 與 JSON:理解 bytes、編碼、訊息邊界和連線生命週期。
  3. 加入 asyncio:把等待網路資料的程式改成 coroutine,觀察多連線下事件迴圈如何切換工作。
  4. 建立 FastAPI API:用資料模型和裝飾器將共用函式提供給網站或 App。
  5. 建立 MCP tools:用型別標註和工具描述,讓 MCP Client 能探索服務能力。
  6. 接上 Agent:讓模型依任務選工具;把權限、核准與業務規則留在 host 和服務層。
  7. 延伸至 AIoT:用 MQTT 接設備資料,將讀取與控制功能包在受控服務層,讓 REST 和 MCP 共用。
安全提醒:AI Agent 的工具呼叫可能造成外部副作用。對泵浦、加熱器、投餌機、電鎖等設備,應採用明確的允許清單、範圍檢查、身分授權、人工確認、操作紀錄與設備端安全互鎖。語言模型不能取代設備控制器的即時保護。

十一、結語

這五篇文章展示的是同一條 Python 學習路徑:從函式與資料開始,再透過 asyncio 處理網路等待,用 FastAPI 和 MCP SDK 將功能轉成不同介面,最後由 Agent 選用工具並連到 AIoT 設備。真正讓系統容易擴充的,是把 Python 語言特性用在清楚的責任分工上:函式承載規則、型別標註說明介面、裝飾器完成註冊、非同步處理 I/O、模組整理專案、資料模型驗證輸入、context manager 管理資源。學會這些基礎後,換成新的 Python 框架或協定,也更容易看懂程式背後的運作方式。

2026年10月25日 星期日

用 Python MCP 實作 Server/Client:從工具註冊到互動呼叫

Python 網路程式設計|MCP 實作

用 Python MCP 實作 Server/Client:從工具註冊到互動呼叫

前一篇以 TCP socket 與 asyncio 建立 echo server/client。本文改用 Model Context Protocol(MCP),讓 server 將功能註冊為工具,client 透過標準協定探索並呼叫工具。範例採 Streamable HTTP,適合兩個獨立程式在本機或區網通訊。

一、MCP 和 TCP 的關係

TCP 是傳輸層通訊協定,負責把位元組可靠地送到對方。MCP 則定義 client 和 server 之間的互動方式,例如初始化、列出工具、呼叫工具,以及回傳內容。MCP 仍需要傳輸方式承載資料;本例使用 Streamable HTTP,底層網路仍會使用 TCP。

本例的差別:一般 echo TCP 程式通常由 client 送一串文字,server 回傳文字。MCP client 會先連線並查詢 server 提供的工具,再用工具名稱與參數呼叫功能。SDK 處理協定訊息、參數驗證及結果格式。
Python MCP Client→Streamable HTTP/TCP→MCP Server→echo_message 工具

官方 Python SDK 目前文件以 v2 為主,要求 Python 3.11 以上,提供 stdio、Streamable HTTP 等傳輸方式。本篇選擇 Streamable HTTP,因為 server 會監聽一個本機埠號,client 可以用 URL 連線。官方 SDK 文件|伺服器執行方式|Client 傳輸方式

二、安裝環境

建立資料夾,放入 mcp_server.py、mcp_client.py 與 requirements.txt。建議使用 Python 3.11 以上版本。

python -m venv .venv

# Windows PowerShell
.venv\\Scripts\\Activate.ps1

# macOS/Linux
source .venv/bin/activate

python -m pip install -r requirements.txt

requirements.txt 的內容:

mcp[cli]>=2.0.0,<3.0.0

其中 mcp 是官方 Python SDK;[cli] 會一併安裝開發用命令列工具。若使用新版 Python 環境管理工具,也可依官方文件以 uv add "mcp[cli]" 安裝。

三、撰寫 MCP Server

from mcp.server import MCPServer

HOST = "127.0.0.1"
PORT = 8000

mcp = MCPServer(
    "Python MCP Echo Server",
    instructions="Provides an echo tool and basic server information.",
)

@mcp.tool()
def echo_message(message: str) -> str:
    """Return the message supplied by the client and log it on the server."""
    print(f"Client message: {message}")
    return f"Server received: {message}"

@mcp.tool()
def get_server_info() -> dict[str, str | int]:
    """Return the name and listening address of this MCP server."""
    return {
        "name": "Python MCP Echo Server",
        "host": HOST,
        "port": PORT,
        "transport": "Streamable HTTP",
    }

if __name__ == "__main__":
    mcp.run(transport="streamable-http", host=HOST, port=PORT)

Server 程式逐段解說

  1. from mcp.server import MCPServer 匯入 SDK 的高階 server 類別。它負責處理 MCP 的初始化、工具清單、呼叫與結果傳輸。
  2. MCPServer(...) 建立 server。第一個參數是供 client 辨識的名稱;instructions 提供 server 的用途說明。
  3. @mcp.tool() 將下一個 Python 函式登記成 MCP 工具。函式名稱成為工具名稱,型別提示用來建立參數規格,docstring 則是工具用途說明。
  4. echo_message(message: str) 接收文字、在 server 終端機印出紀錄,再回傳回應。SDK 會把回傳值包裝成 MCP 工具結果。
  5. get_server_info() 示範回傳結構化資料。client 可以從工具結果取得名稱、位址、埠號和傳輸方式。
  6. mcp.run(transport="streamable-http", ...) 啟動 HTTP 傳輸。預設 MCP endpoint 是 /mcp,所以本機網址為 http://127.0.0.1:8000/mcp。
stdio 與 HTTP:官方 SDK 預設可用 stdio,適合 MCP host 啟動本機子程序;本範例明確指定 Streamable HTTP,因為我們要分別啟動 server 和 client,透過 URL 連線。官方文件將 Streamable HTTP 列為部署型傳輸方式。

四、撰寫 MCP Client

import anyio
from mcp import Client
from mcp.types import TextContent

SERVER_URL = "http://127.0.0.1:8000/mcp"

def show_result(result) -> None:
    if result.is_error:
        print("MCP tool returned an error:")
    for block in result.content:
        if isinstance(block, TextContent):
            print(block.text)
    if result.structured_content is not None:
        print(result.structured_content)

async def main() -> None:
    async with Client(SERVER_URL) as client:
        print(f"Connected to: {SERVER_URL}")

        tools = await client.list_tools()
        print("Available tools:", ", ".join(tool.name for tool in tools.tools))

        info = await client.call_tool("get_server_info", {})
        show_result(info)

        print("Enter a message to call echo_message. Type /quit to exit.")
        while True:
            message = await anyio.to_thread.run_sync(input, "> ")
            if message.strip() == "/quit":
                break
            result = await client.call_tool(
                "echo_message",
                {"message": message},
            )
            show_result(result)

if __name__ == "__main__":
    anyio.run(main)

Client 程式逐段解說

  1. Client(SERVER_URL) 依 URL 選用 Streamable HTTP 傳輸。真正建立連線是在進入 async with 時;離開區塊時 SDK 會關閉連線。
  2. await client.list_tools() 向 server 查詢已註冊工具。這是 MCP 相較自訂 echo 協定的關鍵:client 能探索工具名稱與輸入規格。
  3. await client.call_tool("get_server_info", {}) 呼叫不需參數的工具;第二個參數是工具輸入字典。
  4. await client.call_tool("echo_message", {"message": message}) 以工具名稱與參數送出請求。SDK 負責將呼叫轉成 MCP 訊息並交給 HTTP 傳輸。
  5. result.content 是內容區塊清單。範例只列印文字區塊,因此先用 isinstance(block, TextContent) 確認型別。
  6. result.is_error 可判斷工具是否回報錯誤;result.structured_content 則提供適合程式讀取的結構化結果。
  7. input() 是同步阻塞函式。範例以 anyio.to_thread.run_sync() 在工作執行緒讀取鍵盤,避免互動輸入直接卡住非同步流程。

五、啟動與操作

  1. 開啟第一個終端機,啟用虛擬環境後執行 python mcp_server.py。server 會在 127.0.0.1:8000 提供 MCP endpoint。
  2. 另開第二個終端機,使用同一個環境執行 python mcp_client.py。
  3. client 會列出工具、取得 server 資訊,接著等待輸入訊息。
  4. 輸入文字後按 Enter,client 呼叫 echo_message,server 終端機會顯示收到的文字,client 也會看到回傳內容。
  5. 輸入 /quit 結束 client;在 server 終端機按 Ctrl+C 停止 server。
終端機 1:python mcp_server.py
終端機 2:python mcp_client.py
> Hello MCP
Server received: Hello MCP
> /quit

六、MCP 和一般 TCP/asyncio 的比較

面向一般 TCP socketasyncio TCPMCP
主要工作自行定義資料格式並收發位元組。用協程管理非同步 TCP I/O,資料格式仍要自行定義。以標準化方式描述工具、參數與結果。
連線傳輸TCP socket。TCP socket。可使用 stdio、Streamable HTTP 等標準傳輸;本範例使用 HTTP/TCP。
工具探索通常自行設計命令或文件。通常自行設計命令或文件。client 可列出 server 提供的工具及輸入 schema。
適合情境學習 TCP、簡單客製化協定、低階網路控制。大量 I/O 連線、希望以 async 管理 socket。讓 AI host 或其他 MCP client 以共同介面使用工具、資源與提示。
需留意要處理封包邊界、編碼、錯誤與多連線。仍要處理訊息格式,且要避免阻塞事件迴圈。SDK 會處理協定,但要選對傳輸、安裝相容 SDK,並設計工具權限與輸入驗證。

因此,MCP 不是另一種 TCP,也不是把 socket API 換個名稱。它在 TCP 或 stdio 之上定義可互通的應用層協定。若目標是理解 socket 和網路封包,先學一般 TCP;若目標是把 Python 功能提供給支援 MCP 的 host 或 agent,MCP 會提供更一致的工具介面。

七、常見問題與安全注意事項

  • client 連不上:確認 server 已啟動,URL 使用 /mcp 路徑,且兩端埠號一致。
  • 找不到 mcp 套件:確認安裝套件的 Python 環境與執行程式的環境相同。
  • 為什麼不用自訂 JSON:MCP SDK 已處理協定初始化、訊息格式和工具呼叫;自行用 socket 傳 JSON 不會自動成為 MCP server。
  • 連區網:可將 server host 設為 0.0.0.0,client 改連 server 電腦的區網 IP,並設定防火牆。對外提供服務時應採用 HTTPS、驗證與授權。
  • 工具安全:正式工具要驗證輸入、限制可執行的操作與存取範圍。不要把未驗證且有敏感副作用的工具直接開放給不受信任的 client。

結語

使用 MCP 後,server 不再只接收一串自訂文字,而是公開具名稱、描述和參數規格的工具;client 能先探索工具,再呼叫指定功能。Streamable HTTP 讓兩個獨立程式可透過 URL 通訊,而 MCP SDK 負責協定細節。這讓範例從「傳送文字」進一步成為「用標準介面呼叫服務功能」。