2026年8月31日 星期一

Python一下:用魚菜共生監測程式認識內部函式與Closure

Python一下|第8篇

用魚菜共生監測程式認識內部函式與Closure

《從風土資料到智慧生活的程式設計》——讓每個監測規則記住自己的名稱、上下限與執行狀態。

本篇學習目標
  • 理解區域變數、外層變數與全域變數。
  • 在函式內定義及呼叫內部函式。
  • 把內部函式作為回傳值。
  • 理解Closure如何保存外層函式的環境。
  • 使用nonlocal更新外層函式狀態。
  • 建立可重複產生水溫、溶氧與酸鹼值規則的監測工廠。

一、為什麼要學內部函式與Closure?

魚菜共生系統同時監測水溫、溶氧、酸鹼值等資料。它們的判讀流程相似,但每個項目的名稱、單位與示範上下限不同。若每個項目都複製一份程式,日後容易修改不一致。

Closure可以先建立一個「規則工廠」,再由它產生各自記住設定的監測函式:

水溫規則

記住名稱、單位及上下限

溶氧規則

有自己的設定與計數

酸鹼值規則

共用結構,不共用狀態

二、先理解變數作用域

變數在哪裡建立,通常就決定它可以在哪裡使用。Python查找名稱時,可先用LEGB概念理解:

層級英文意義
LLocal目前函式內的區域名稱
EEnclosing外層函式中的名稱
GGlobal模組最外層的全域名稱
BBuilt-inPython內建名稱,如printlen
system_name = "魚菜共生監測系統"  # Global


def show_sensor():
    sensor_name = "水溫"  # Local
    print(system_name)
    print(sensor_name)


show_sensor()

函式內可以讀取外層的system_name;但函式外不能直接讀取函式內的sensor_name

避免覆蓋內建名稱:不要把變數命名為liststrfiltersum,否則可能暫時遮蔽Python內建功能。

三、內部函式:只服務外層任務

內部函式是在另一個函式中定義的函式。若某個小功能只屬於外層流程,放在內部可以限制使用範圍。

def build_report(sensor_name, value, unit):
    def format_value(number):
        return f"{number:.1f} {unit}"

    return f"{sensor_name}:{format_value(value)}"


print(build_report("水溫", 28.56, "°C"))

format_value()只能在build_report()執行期間由其內部使用,而且能讀取外層的unit

為什麼不全部寫成獨立函式?

內部函式適合獨立函式適合
只服務單一外層流程會被多個模組或流程共用
需要自然讀取外層設定參數與回傳值可清楚表達
不希望成為公開介面需要獨立測試與文件

四、函式也可以回傳函式

前一篇已學到函式可以當成物件。現在讓外層函式把內部函式回傳出去:

def make_formatter(unit):
    def format_value(value):
        return f"{value:.1f} {unit}"

    return format_value


temperature_formatter = make_formatter("°C")
oxygen_formatter = make_formatter("mg/L")

print(temperature_formatter(28.56))
print(oxygen_formatter(5.84))
注意:return format_value沒有括號,代表回傳函式本身;若寫return format_value(value),則是執行後回傳結果。

五、Closure是什麼?

外層make_formatter()早已執行完畢,但回傳的format_value()仍記得當時的unit。這種「函式連同它所需要的外層環境一起被保留下來」的現象,就是Closure(閉包)。

一句話:Closure是會記住外層環境的函式。

每個Closure保存自己的設定

def make_checker(sensor_name, minimum, maximum, unit):
    def check(value):
        in_range = minimum <= value <= maximum
        return {
            "項目": sensor_name,
            "數值": value,
            "單位": unit,
            "教學範圍內": in_range
        }

    return check


check_temperature = make_checker("水溫", 25, 30, "°C")
check_oxygen = make_checker("溶氧", 5, 10, "mg/L")

print(check_temperature(28.5))
print(check_oxygen(4.8))

check_temperaturecheck_oxygen來自同一個工廠,但各自記住不同設定。

門檻提醒:上例數字只為示範Closure與條件判斷,不是魚菜共生或養殖管理的正式標準。真實規則須依物種、系統、季節及專業意見設定。

六、查看Closure記住了什麼

學習與除錯時,可以查看函式的__closure__

def make_multiplier(factor):
    def multiply(value):
        return value * factor

    return multiply


double = make_multiplier(2)

print(double.__closure__)
print([cell.cell_contents for cell in double.__closure__])

這是觀察Closure的學習工具,不建議讓正式程式依賴這些內部細節來修改狀態。

七、nonlocal:更新外層函式狀態

讀取外層變數不需特別宣告;若要在內部函式重新指定外層變數,則使用nonlocal

def make_counter(sensor_name):
    count = 0

    def record():
        nonlocal count
        count += 1
        return f"{sensor_name}已記錄{count}次"

    return record


temperature_counter = make_counter("水溫")

print(temperature_counter())
print(temperature_counter())
print(temperature_counter())
水溫已記錄1次 水溫已記錄2次 水溫已記錄3次

沒有nonlocal會發生什麼?

def make_broken_counter():
    count = 0

    def record():
        # count += 1 會被視為建立區域變數,
        # 但在指定前又嘗試讀取它,因此會出錯。
        return count

    return record
nonlocal和global不同:nonlocal尋找最近的外層函式變數;global指向模組最外層名稱。Closure通常應使用nonlocal管理自己的狀態,避免多個規則共用全域變數。

八、可變物件與Closure

若外層變數是List或Dict,內部函式只修改其內容而不重新指定名稱,通常不需nonlocal

def make_recorder(sensor_name):
    records = []

    def record(value):
        records.append(value)
        return {
            "項目": sensor_name,
            "筆數": len(records),
            "最新值": records[-1]
        }

    return record


record_temperature = make_recorder("水溫")
print(record_temperature(28.5))
print(record_temperature(28.8))
狀態管理:Closure內的List會持續保存資料。若紀錄無限增加,可能占用記憶體;正式監測系統應考慮資料庫、檔案、保留期限及容量限制。

九、完整示範:魚菜共生監測規則工廠

def make_monitor(sensor_name, minimum, maximum, unit):
    check_count = 0
    attention_count = 0

    def monitor(value):
        nonlocal check_count, attention_count
        check_count += 1

        if value is None:
            status = "資料缺漏"
            usable = False
        elif minimum <= value <= maximum:
            status = "位於教學示範範圍"
            usable = True
        else:
            status = "需進一步確認"
            usable = True
            attention_count += 1

        return {
            "項目": sensor_name,
            "數值": value,
            "單位": unit,
            "狀態": status,
            "可供判讀": usable,
            "累計檢查": check_count,
            "累計需確認": attention_count
        }

    return monitor


monitor_temperature = make_monitor("水溫", 25, 30, "°C")
monitor_oxygen = make_monitor("溶氧", 5, 10, "mg/L")

print(monitor_temperature(28.5))
print(monitor_temperature(31.2))
print(monitor_temperature(None))

print(monitor_oxygen(5.8))
print(monitor_oxygen(4.7))
觀察重點:兩個監測器共用同一套程式結構,卻各自保存名稱、單位、上下限、累計檢查次數與需確認次數。這正是Closure用來建立「帶設定與狀態的函式」的價值。

十、Closure、類別與全域變數怎麼選?

方式適合情況優點注意
Closure少量設定與狀態,主要提供一個操作簡潔、封裝外層變數狀態不如類別明顯
類別多個屬性、方法及生命週期結構清楚,容易擴充初始程式較多
全域變數真正全域且近似常數的設定存取直接可變全域狀態難追蹤,不利測試

若監測器未來要加入重新校正、清除紀錄、輸出報表等多個操作,類別通常比Closure更合適。

十一、常見錯誤

錯誤原因修正
外部直接呼叫內部函式名稱名稱只存在於外層函式作用域由外層回傳內部函式
return inner()取代return inner回傳了執行結果,不是函式要建立Closure時不加括號
更新外層整數時出現UnboundLocalError缺少nonlocal重新指定外層變數前宣告nonlocal
所有監測器共用一個全域計數狀態沒有隔離每次呼叫工廠建立獨立Closure
Closure保存大量無上限資料狀態持續存在限制容量或改用持久化儲存
Closure邏輯過於複雜封裝層次不清楚改用類別或拆分模組

十二、漸進式練習

基礎:單位格式工廠

建立make_formatter(unit),分別產生水溫、溶氧與鹽度格式函式。

進階:獨立計數器

建立水溫與溶氧兩個計數器,交錯呼叫並驗證兩者是否各自保存次數。

USR挑戰:可追溯規則

為規則工廠增加「規則版本」與「建立者」設定,讓每次回傳結果都包含判讀依據。再思考:正式系統若需修改規則、追蹤歷史版本及多人管理,Closure是否仍是最合適選擇?

十三、學習檢核

  1. LEGB四個字母各代表哪一層作用域?
  2. 內部函式何時比獨立函式合適?
  3. return innerreturn inner()有何差異?
  4. Closure為什麼在外層函式結束後仍記得設定?
  5. 何時需要nonlocal
  6. Closure內保存大量監測紀錄可能產生什麼問題?
  7. 何時應改用類別而不是Closure?

十四、AI協作提示詞

請擔任Python作用域與Closure助教。
我要把多個USR監測項目的共同邏輯整理成規則工廠。
請先詢問每個規則要保存的名稱、單位、設定、狀態與操作。

請依序提供:
1. 不使用Closure的一般函式版本;
2. 內部函式版本;
3. 回傳內部函式形成Closure;
4. 需要更新狀態時才加入nonlocal;
5. 說明何時應改用類別。

所有場域門檻都必須標示為教學示範,
不可捏造真實養殖標準或USR成果。

十五、延伸閱讀

十六、本篇小結

內部函式能把只服務某項工作的細節封裝在外層函式中;Closure則讓回傳的函式繼續記住外層設定與狀態。它很適合建立輕量的格式器、規則與計數器,但當功能、狀態與版本管理變複雜時,就應考慮類別及資料庫。

Closure能記住程式設定;USR系統還必須記住規則來源、修改歷程與人的專業判斷。

沒有留言:

張貼留言