2026年11月11日 星期三

Apache Spark與PySpark入門:當智慧養殖感測資料變成大數據,Python該如何處理?

Apache Spark與PySpark入門:當智慧養殖感測資料變成大數據,Python該如何處理?

一座智慧養殖池每分鐘記錄水溫、pH與溶氧,資料量看起來不大;若擴展至多座養殖池、數百個感測器,持續記錄多年,資料就可能成為管理上的挑戰。上一篇我們利用Pandas清理與分析感測資料,這次要認識Apache Spark及其Python介面PySpark,了解何時需要分散式資料處理。

本篇學習目標:理解Pandas與Spark的差異、建立本機PySpark環境、使用DataFrame清理與彙整智慧養殖資料,並認識Spark與IoT資料平台的整合方式。

Python Pandas實戰:從智慧養殖感測資料學習資料清理、統計與視覺化

Python Pandas實戰:從智慧養殖感測資料學習資料清理、統計與視覺化

智慧養殖系統中的感測器能持續記錄水溫、酸鹼值(pH)及溶氧量(DO)。但感測器傳來的資料不一定完整:可能漏值、重複上傳,甚至出現不合理的讀數。這篇文章將從一份小型模擬資料開始,透過Python Pandas學習「資料讀取 → 資料清理 → 統計分析 → 視覺化 → 結果輸出」,為後續AIoT與大數據分析打好基礎。

學習對象:已具備Python變數、串列、字典與基本函式概念的學生。準備工具:Python、Pandas、Matplotlib。本文數值均為教學模擬,不是水井村真實量測紀錄,也不構成養殖操作門檻建議。

從物聯網到大數據:IoT、Big Data與AIoT有什麼關係?

從物聯網到大數據:IoT、Big Data與AIoT有什麼關係?

智慧生活科技專業社群|Python × IoT × Big Data × AIoT|2026年11月

一個感測器可以量測溫度;一套物聯網可以遠端監控;當長期累積的資料經過分析,才可能進一步協助預測與決策。這就是從物聯網走向大數據與AIoT的學習路徑。

本文延續社群既有的 micro:bit、MQTT、Pico 2 W、Django、Python資料分析及Ollama文章,並以「智慧養殖池」作為共同情境,說明這些技術如何串接。文中數值均為教學模擬,並非實際養殖場域量測資料。

一、先從生活問題開始:養殖池需要知道什麼?

想像養殖管理者希望隨時掌握水溫、酸鹼值(pH)、溶氧(DO)與鹽度。傳統方式需要到現場讀取儀表;如果裝上感測器與連網裝置,就能定期傳送資料,並透過手機或網站查看。

  • IoT:負責「感測、連線與傳送」。
  • Big Data:關注大量、快速、多樣資料的儲存、治理與分析。
  • AIoT:結合AI與IoT,利用感測資料進行異常辨識、預測或輔助控制。

三者不是互相取代的產品,也不是所有IoT系統都必須使用大數據平台。小型養殖池用一般資料庫與Pandas就可能足夠;只有當資料量、速度或處理複雜度增加,才需要考慮Spark等分散式技術。

二、IoT是什麼?讓真實世界的設備能交換資料

物聯網(Internet of Things,IoT)是讓感測器、控制器及其他實體設備透過網路交換資料的系統。以養殖池為例,水溫感測器量測數值,ESP32或Raspberry Pi Pico W讀取資料,經由Wi-Fi送往伺服器。

感測器 → ESP32 / Pico W → Wi-Fi → MQTT或HTTP → 雲端伺服器 → Dashboard

在社群既有文章中,〈micro:bit和電腦的串列通訊程式〉先示範將溫度值傳給電腦;接著〈micro:bit和電腦的串列通訊程式加上MQTT〉,讓資料能透過訊息代理伺服器交換。這是由「單機讀值」進入「網路資料傳輸」的具體例子。

MQTT與HTTP有什麼不同?

比較項目MQTTHTTP / REST API
常見模式發布/訂閱(Publish/Subscribe)請求/回應(Request/Response)
主要用途設備訊息與即時事件傳遞讀取或提交Web服務資料
系統角色Client、Broker、TopicClient、Server、URL
養殖應用定期發布感測數值、傳送告警查詢歷史資料、上傳量測紀錄

想了解裝置如何呼叫Web API,可接著閱讀〈HTTP與REST API|Pico 2 W〉,其中示範使用GET讀取資料、POST送出JSON,以及處理HTTP狀態碼與網路例外。

三、為什麼IoT資料會走向Big Data?

一筆水溫資料只是一次量測;每分鐘記錄一次,一台設備每天會產生1,440筆。如果有100台設備,每台各有4項感測數值,而且每項數值都以獨立紀錄儲存,則每天可能產生:

100 × 4 × 1,440 = 576,000筆/天

一年約為210,240,000筆(按365天計算)。這是說明資料成長速度的假設情境,實際筆數會受到傳送頻率、封包設計、遺失資料與設備數量影響。

不過,資料筆數多,不必然代表必須使用大數據技術。真正的重點是現有資料庫與分析工具能否在合理成本、時間與可靠度下完成工作。

認識大數據的5V

  • Volume(大量):長期累積的設備與歷史資料。
  • Velocity(高速):每秒或每分鐘持續進來的感測訊息。
  • Variety(多樣):數值、JSON、影像、設備日誌與維修紀錄。
  • Veracity(可信度):感測器校正、缺值、異常值及資料品質。
  • Value(價值):能否協助節能、節水、管理與決策。

因此,大數據不是把資料「存得越多越好」,而是要能把資料整理成可理解、可追溯、可應用的資訊。

四、從IoT到資料分析:Python與Pandas先上場

在開始學習Spark以前,先掌握Python資料處理是更合理的順序。社群文章〈讓ChatGPT產生樞紐分析表來分析漁貨交易資料〉,已示範使用Pandas讀取Excel,依市場與魚貨名稱統計交易量與平均價格。

我們也可以將同樣的觀念套用到養殖水溫。以下範例不需要感測器,也不需要雲端帳號;安裝Pandas後即可在電腦執行。

import pandas as pd

# 教學用模擬資料,非真實量測
records = [
    {"pond": "A", "temperature": 27.5, "oxygen": 6.2},
    {"pond": "A", "temperature": 28.0, "oxygen": 5.8},
    {"pond": "B", "temperature": 29.1, "oxygen": 4.9},
    {"pond": "B", "temperature": 28.7, "oxygen": 5.1},
]

df = pd.DataFrame(records)

summary = df.groupby("pond").agg(
    avg_temperature=("temperature", "mean"),
    min_oxygen=("oxygen", "min")
)

print(summary)

預期輸出:

      avg_temperature  min_oxygen
pond
A               27.75         5.8
B               28.90         4.9

這段程式完成兩件事:計算各池平均水溫,並找出各池最低溶氧。這是描述性資料分析,還不是AI預測。是否需要告警,必須再依養殖物種、環境條件及專業管理標準設定判斷規則,不能單憑這四筆模擬資料下結論。

五、什麼時候需要Apache Spark?

Apache Spark是一套可進行大規模資料處理的分散式運算引擎,常用於批次資料處理、SQL分析與機器學習工作流程。它能把工作分配給多個運算節點,但也可以先在單機環境練習。

情境建議起點
幾千到數十萬筆教學資料Python+Pandas
單一或少量養殖場的歷史資料關聯式或時序資料庫+Pandas
多場域、長期大量資料,單機處理成為瓶頸評估Spark、分散式儲存及資料管線
即時事件與告警MQTT+事件處理;視需求評估串流架構

以上是教學上的選型方向,並非依資料筆數劃定的硬性門檻。Spark也不是資料庫或MQTT Broker的直接替代品;它通常與資料儲存、資料管線及其他服務協同運作。

未來可以延伸一篇〈PySpark實戰:分析大量智慧養殖感測資料〉,讓學生比較相同的分組統計在Pandas與Spark DataFrame中的寫法與運算方式。

六、AIoT是什麼?讓系統不只看見數據

AIoT(Artificial Intelligence of Things)結合人工智慧與物聯網。IoT負責取得現場資訊,AI則可在資料品質足夠、模型經過驗證的前提下,辨識模式、預測風險或提出輔助建議。

以智慧養殖為例,可將功能分成四個層次:

  1. 看得到:Dashboard顯示目前水溫、pH與溶氧。
  2. 查得到:資料庫保存歷史紀錄,查詢每日、每週變化。
  3. 看得懂:分析趨勢、缺值、異常波動及不同池的差異。
  4. 能協助判斷:經驗證的模型提出風險預警,並由管理者確認是否採取行動。

社群文章〈利用Ollama分析異常數據並提供預測性維護設備的建議〉,示範將異常資料交給地端大語言模型解讀。這類LLM適合協助摘要與解釋,但LLM產生的建議不等於已經訓練、驗證的預測模型;涉及設備控制或養殖安全時,仍須結合量測證據、規則與人工審核。

七、把技術整合起來:智慧養殖資料流

感測層
水溫、pH、溶氧、鹽度感測器
↓
邊緣裝置
ESP32 / Raspberry Pi Pico W
↓
通訊層
Wi-Fi、MQTT、HTTP / REST API
↓
平台與資料層
Django、資料庫、資料品質檢查
↓
分析層
Pandas;規模需要時評估Spark
↓
應用層
Dashboard、異常告警、AI輔助決策

其中,〈[Django 6.0] 整合Django和MQTT〉示範了Django與MQTT訊息的整合,正好可作為通訊層銜接Web應用的參考。正式部署還需處理認證、TLS加密、資料庫寫入、設備識別、訊息重送及監控等問題。

八、大學生可以怎麼學?四階段專題

  1. 第一階段:裝置連線。用micro:bit或Pico W取得溫度,確認序列埠、Wi-Fi與資料格式。
  2. 第二階段:資料上雲。使用MQTT或HTTP傳送JSON,並在Django建立資料接收與查詢功能。
  3. 第三階段:資料分析。匯出CSV,使用Pandas計算平均、極值、缺值比例及時間趨勢。
  4. 第四階段:智慧應用。設計有依據的告警規則,評估模型效能,再逐步導入AI輔助解讀。

建議學生在報告中清楚標示:資料是實測還是模擬?量測單位是什麼?資料缺漏如何處理?AI的輸出是否經過驗證?這些問題往往比使用哪一套熱門工具更重要。

九、延伸閱讀:智慧生活科技專業社群既有文章

  1. micro:bit和電腦的串列通訊程式:感測資料進入電腦的第一步。
  2. micro:bit和電腦的串列通訊程式加上MQTT:發布/訂閱訊息傳遞。
  3. HTTP與REST API|Pico 2 W:IoT裝置呼叫遠端API。
  4. [Django 6.0] 整合Django和MQTT:Web平台整合訊息。
  5. 讓ChatGPT產生樞紐分析表來分析漁貨交易資料:Pandas資料彙整。
  6. 利用Ollama分析異常數據並提供預測性維護設備的建議:LLM輔助解讀。

十、結語:從感測資料走向有價值的智慧決策

IoT讓我們能蒐集真實世界的資訊;Big Data提供管理與分析大規模資料的思維和工具;AIoT則嘗試讓資料成為可採取行動的洞察。三者的關係不是「設備越多、資料越大、AI越強」就一定越好,而是從真實需求出發,建立可信的資料流程,再選擇適合的分析與智慧技術。

對初學者而言,先讓一顆感測器成功傳資料,再用Python整理幾十筆紀錄,就是很好的起點。當學生理解資料從哪裡來、如何傳送、怎麼儲存、如何分析,才真正具備把IoT、Big Data與AIoT整合到智慧生活場域的能力。

下一篇預告:《Python Pandas實戰:從智慧養殖感測資料學習資料清理、統計與視覺化》。

教學與AI使用說明:本文依據社群既有公開文章進行延伸整理,使用生成式AI協助架構、文字與示範程式草擬;模擬資料僅供教學。實際應用應由教師及場域專業人員確認設備、資料與控制安全。

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 協作