2026年8月31日 星期一

Python一下:感測資料出錯怎麼辦?用例外處理守住智慧生活程式

《Python一下:從風土資料到智慧生活》第 13 篇

感測資料出錯怎麼辦?用例外處理守住智慧生活程式

錯誤不一定能避免,但可以被辨識、記錄、隔離,並交給正確的人處理。

CH8 例外錯誤處理智慧養殖
學習目標
完成本篇後,你能辨認常見例外,正確使用 tryexceptelsefinallyraise,建立自訂例外,並讓整批巡查資料在單筆失敗時仍能繼續處理。

一、錯誤不是同一種錯誤

程式可能因語法、資料或執行環境而失敗。例外處理主要面對「語法正確,但執行時發生的異常狀況」。

類型例子處理方向
語法錯誤漏掉冒號、括號不成對先修改程式,不能靠 try 掩蓋
資料錯誤"故障" 無法轉成浮點數驗證輸入並捕捉特定例外
資源錯誤檔案不存在、網路逾時提供重試、替代流程或清楚訊息
程式設計錯誤變數名稱拼錯、邏輯判斷錯誤修正與測試,不應靜默忽略
安全原則:捕捉例外不是把錯誤藏起來。智慧養殖系統遇到不可信資料時,應標示「未知」或「待確認」,保留錯誤紀錄並通知人員;不可把失敗讀值自動當成正常值,更不可直接觸發高風險設備操作。

二、先看沒有保護的程式

raw_value = input("請輸入水溫:")
temperature = float(raw_value)
print(f"本次讀值:{temperature}°C")

如果輸入「感測器故障」,float() 會產生 ValueError,程式立即中止。

三、try 與 except:接住預期中的問題

raw_value = input("請輸入水溫:")

try:
    temperature = float(raw_value)
except ValueError:
    print("讀值不是有效數字,請檢查輸入或感測器。")
else:
    print(f"本次讀值:{temperature}°C")

try 只放可能失敗的程式;except ValueError 只處理預期的轉換錯誤;else 則在沒有發生例外時執行。

四、分別處理不同例外

缺少字典欄位會產生 KeyError,內容不能轉成數字則是 ValueErrorTypeError。分開處理,訊息才有診斷價值。

def parse_reading(record):
    try:
        site_name = record["場域"]
        temperature = float(record["水溫"])
        ph = float(record["pH"])
    except KeyError as error:
        missing_key = error.args[0]
        return None, f"缺少欄位:{missing_key}"
    except (TypeError, ValueError) as error:
        return None, f"數值格式錯誤:{error}"
    else:
        reading = {
            "場域": site_name,
            "水溫": temperature,
            "pH": ph,
        }
        return reading, None


data = {"場域": "示範池 A", "水溫": "27.2", "pH": "7.4"}
reading, error = parse_reading(data)
print(reading if error is None else error)

五、finally:無論成功失敗都要收尾

finally 適合釋放資源,例如關閉設備連線。實際檔案處理優先使用 with,因為它會自動管理關閉動作。

class DemoConnection:
    def close(self):
        print("連線已關閉")


connection = DemoConnection()

try:
    print("讀取示範感測資料")
    value = float("27.2")
    print(value)
except ValueError:
    print("讀值格式錯誤")
finally:
    connection.close()
四段流程:try 嘗試工作;except 處理失敗;else 處理成功後的工作;finally 無論如何都收尾。不是每次都要四段全部使用。

六、raise:主動拒絕不合理資料

資料雖然可以轉成數字,仍可能不符合基本格式範圍。函式可主動拋出例外,要求呼叫端處理。

def validate_ph(value):
    ph = float(value)
    if not 0.0 <= ph <= 14.0:
        raise ValueError("pH 讀值超出 0 到 14 的理論範圍")
    return ph


try:
    ph = validate_ph("15.2")
except ValueError as error:
    print("資料未採用:", error)
else:
    print("pH 格式驗證完成:", ph)

這裡只驗證 pH 的基本理論範圍,不代表養殖安全門檻。業務規則應由養殖戶與專業人員另行確認。

七、建立有意義的自訂例外

自訂例外能區分「設備離線」與一般數值錯誤,也讓上層程式決定是否重試、通知或略過。

class SensorError(Exception):
    """感測器相關錯誤的基底類別。"""


class SensorOfflineError(SensorError):
    """感測器目前無法連線。"""


class InvalidReadingError(SensorError):
    """感測值存在,但內容不可信。"""


def read_demo_sensor(sensor_id, status, raw_value):
    if status != "online":
        raise SensorOfflineError(f"{sensor_id} 離線")

    try:
        return float(raw_value)
    except (TypeError, ValueError) as error:
        raise InvalidReadingError(
            f"{sensor_id} 讀值無效:{raw_value!r}"
        ) from error


try:
    value = read_demo_sensor("TEMP-01", "online", "故障")
except SensorOfflineError as error:
    print("請檢查連線:", error)
except InvalidReadingError as error:
    print("請檢查感測器:", error)

raise ... from error 保留原始錯誤原因,除錯時能看到例外之間的關聯。

八、讓單筆失敗不拖垮整批巡查

批次處理時,把 try 放在迴圈內,個別資料失敗後仍可繼續;同時把成功資料與錯誤紀錄分開保存。

def parse_record(record):
    site = record["場域"]
    temperature = float(record["水溫"])
    ph = float(record["pH"])
    if not 0.0 <= ph <= 14.0:
        raise ValueError("pH 超出理論範圍")
    return {
        "場域": site,
        "水溫": temperature,
        "pH": ph,
    }


records = [
    {"場域": "示範池 A", "水溫": "27.2", "pH": "7.4"},
    {"場域": "示範池 B", "水溫": "離線", "pH": "7.1"},
    {"場域": "示範池 C", "水溫": "26.8"},
]

valid_records = []
error_log = []

for index, record in enumerate(records, start=1):
    try:
        valid_records.append(parse_record(record))
    except KeyError as error:
        error_log.append(f"第 {index} 筆缺少欄位:{error.args[0]}")
    except (TypeError, ValueError) as error:
        error_log.append(f"第 {index} 筆格式錯誤:{error}")

print("成功筆數:", len(valid_records))
print("失敗筆數:", len(error_log))
for message in error_log:
    print("-", message)

九、檔案不存在時怎麼辦?

讀取設定檔時,處理可預期的 FileNotFoundErrorjson.JSONDecodeError。不要用空白的 except: 把所有錯誤吞掉。

import json
from pathlib import Path


def load_config(filename):
    path = Path(filename)
    try:
        with path.open("r", encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError:
        print(f"找不到設定檔:{path}")
    except json.JSONDecodeError as error:
        print(f"JSON 格式錯誤,行 {error.lineno}:{error.msg}")
    return None


config = load_config("site_config.json")
if config is None:
    print("停止自動控制,改由人員確認設定。")

十、記錄錯誤,不只 print

正式程式可使用標準函式庫 logging,保留時間、等級與訊息。公開紀錄前應移除帳號、Token、精確位置及個人資料。

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
)

try:
    value = float("離線")
except ValueError:
    logging.exception("示範感測值轉換失敗")
else:
    logging.info("示範感測值已完成轉換:%s", value)

十一、不要這樣捕捉例外

# 不建議:錯誤種類與原因全部消失
try:
    value = float("離線")
except:
    pass

若確實需要在系統邊界捕捉未知例外,可用 except Exception as error 記錄後再拋出,或進入明確的安全狀態;不要假裝工作成功。

十二、例外處理決策表

問題建議動作不建議
使用者輸入格式錯誤說明格式並允許重新輸入默認為 0
單一感測器離線標記未知、記錄、通知並繼續其他設備沿用舊值卻不標示
關鍵設定檔損壞停止自動控制並要求人工確認自行猜測控制參數
程式設計錯誤記錄完整資訊、測試並修正用空白 except 隱藏

十三、USR 實作挑戰

挑戰 A|巡查資料清洗
建立十筆含空白、缺欄與錯誤文字的教學資料,輸出成功清單與錯誤報告,確認任何單筆問題都不會讓整批中止。
挑戰 B|安全降級
設計感測器離線時的流程:標示未知、停止自動判斷、通知巡查人員,但仍顯示其他設備的有效讀值。
挑戰 C|錯誤訊息共創
請養殖戶協助檢視錯誤訊息是否看得懂、是否知道下一步怎麼做,再依回饋修改文字。

十四、與生成式 AI 協作

請擔任 Python 例外處理與安全設計助教。
請檢查我的智慧養殖資料處理程式:
1. 指出可能出現的具體例外;
2. 只捕捉可以合理處理的例外;
3. 不可使用 except: pass;
4. 單筆錯誤不能拖垮整批資料;
5. 無效讀值必須標示未知,不可假裝正常;
6. 建議應記錄哪些資訊,以及哪些敏感資訊不可寫入日誌。
請先說明風險,再提供修正版程式。
本篇小結
好的例外處理不是讓程式「永遠不停止」,而是讓失敗變得可辨識、可追查且可安全處置。捕捉範圍愈精準,錯誤訊息愈具體,現場人員愈能做出正確決定。

沒有留言:

張貼留言