用魚菜共生監測程式認識內部函式與Closure
《從風土資料到智慧生活的程式設計》——讓每個監測規則記住自己的名稱、上下限與執行狀態。
- 理解區域變數、外層變數與全域變數。
- 在函式內定義及呼叫內部函式。
- 把內部函式作為回傳值。
- 理解Closure如何保存外層函式的環境。
- 使用
nonlocal更新外層函式狀態。 - 建立可重複產生水溫、溶氧與酸鹼值規則的監測工廠。
一、為什麼要學內部函式與Closure?
魚菜共生系統同時監測水溫、溶氧、酸鹼值等資料。它們的判讀流程相似,但每個項目的名稱、單位與示範上下限不同。若每個項目都複製一份程式,日後容易修改不一致。
Closure可以先建立一個「規則工廠」,再由它產生各自記住設定的監測函式:
水溫規則
記住名稱、單位及上下限
溶氧規則
有自己的設定與計數
酸鹼值規則
共用結構,不共用狀態
二、先理解變數作用域
變數在哪裡建立,通常就決定它可以在哪裡使用。Python查找名稱時,可先用LEGB概念理解:
| 層級 | 英文 | 意義 |
|---|---|---|
| L | Local | 目前函式內的區域名稱 |
| E | Enclosing | 外層函式中的名稱 |
| G | Global | 模組最外層的全域名稱 |
| B | Built-in | Python內建名稱,如print、len |
system_name = "魚菜共生監測系統" # Global
def show_sensor():
sensor_name = "水溫" # Local
print(system_name)
print(sensor_name)
show_sensor()函式內可以讀取外層的system_name;但函式外不能直接讀取函式內的sensor_name。
list、str、filter或sum,否則可能暫時遮蔽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保存自己的設定
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_temperature與check_oxygen來自同一個工廠,但各自記住不同設定。
六、查看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())沒有nonlocal會發生什麼?
def make_broken_counter():
count = 0
def record():
# count += 1 會被視為建立區域變數,
# 但在指定前又嘗試讀取它,因此會出錯。
return count
return recordnonlocal尋找最近的外層函式變數;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))九、完整示範:魚菜共生監測規則工廠
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更合適。
十一、常見錯誤
| 錯誤 | 原因 | 修正 |
|---|---|---|
| 外部直接呼叫內部函式名稱 | 名稱只存在於外層函式作用域 | 由外層回傳內部函式 |
return inner()取代return inner | 回傳了執行結果,不是函式 | 要建立Closure時不加括號 |
| 更新外層整數時出現UnboundLocalError | 缺少nonlocal | 重新指定外層變數前宣告nonlocal |
| 所有監測器共用一個全域計數 | 狀態沒有隔離 | 每次呼叫工廠建立獨立Closure |
| Closure保存大量無上限資料 | 狀態持續存在 | 限制容量或改用持久化儲存 |
| Closure邏輯過於複雜 | 封裝層次不清楚 | 改用類別或拆分模組 |
十二、漸進式練習
基礎:單位格式工廠
建立make_formatter(unit),分別產生水溫、溶氧與鹽度格式函式。
進階:獨立計數器
建立水溫與溶氧兩個計數器,交錯呼叫並驗證兩者是否各自保存次數。
USR挑戰:可追溯規則
為規則工廠增加「規則版本」與「建立者」設定,讓每次回傳結果都包含判讀依據。再思考:正式系統若需修改規則、追蹤歷史版本及多人管理,Closure是否仍是最合適選擇?
十三、學習檢核
- LEGB四個字母各代表哪一層作用域?
- 內部函式何時比獨立函式合適?
return inner與return inner()有何差異?- Closure為什麼在外層函式結束後仍記得設定?
- 何時需要
nonlocal? - Closure內保存大量監測紀錄可能產生什麼問題?
- 何時應改用類別而不是Closure?
十四、AI協作提示詞
請擔任Python作用域與Closure助教。
我要把多個USR監測項目的共同邏輯整理成規則工廠。
請先詢問每個規則要保存的名稱、單位、設定、狀態與操作。
請依序提供:
1. 不使用Closure的一般函式版本;
2. 內部函式版本;
3. 回傳內部函式形成Closure;
4. 需要更新狀態時才加入nonlocal;
5. 說明何時應改用類別。
所有場域門檻都必須標示為教學示範,
不可捏造真實養殖標準或USR成果。十五、延伸閱讀
十六、本篇小結
內部函式能把只服務某項工作的細節封裝在外層函式中;Closure則讓回傳的函式繼續記住外層設定與狀態。它很適合建立輕量的格式器、規則與計數器,但當功能、狀態與版本管理變複雜時,就應考慮類別及資料庫。
Closure能記住程式設定;USR系統還必須記住規則來源、修改歷程與人的專業判斷。
沒有留言:
張貼留言