用Decorator記錄水井USR設備操作歷程
《從風土資料到智慧生活的程式設計》——不改動核心函式,也能一致加入紀錄、計時與錯誤追蹤。
- 理解Decorator是「接收函式並回傳新函式」的高階函式。
- 從手動包裝推導
@decorator語法。 - 使用
*args與**kwargs包裝不同參數的函式。 - 使用
functools.wraps保存原函式資訊。 - 記錄操作時間、函式名稱、結果及錯誤。
- 避免在紀錄中暴露密碼、Token、居民個資與敏感場域資訊。
一、為什麼設備操作需要紀錄?
水井USR的智慧養殖、魚菜共生、故事機與網站服務都可能包含設備操作。當程式執行「讀取感測器、切換模式、更新資料」時,若能留下適當紀錄,就較容易回答:
何時
操作與錯誤發生時間
做了什麼
呼叫哪個功能與結果
是否成功
耗時、失敗類型及追查線索
如果每個函式都自行撰寫相同的紀錄程式,會大量重複。Decorator能把這類「橫跨多個功能的共同工作」集中管理。
二、先看沒有Decorator的寫法
def read_temperature(pond_name):
return f"{pond_name}水溫讀取完成"
print("開始執行read_temperature")
result = read_temperature("一號池")
print("執行結果:", result)
print("結束執行read_temperature")若十個函式都需要相同的開始、結果與結束訊息,就會複製十次。更好的方式是建立一個包裝函式。
三、手動包裝函式
def add_log(func):
def wrapper():
print(f"開始執行:{func.__name__}")
result = func()
print(f"執行結果:{result}")
print(f"結束執行:{func.__name__}")
return result
return wrapper
def check_gateway():
return "閘道器連線正常"
decorated_function = add_log(check_gateway)
decorated_function()add_log()接收原函式,建立wrapper()加入新行為,再回傳包裝後的函式。這就是Decorator的核心結構。
四、@decorator語法糖
下列兩種寫法效果相同:
| 手動包裝 | Decorator語法 |
|---|---|
check_gateway = add_log(check_gateway) | @add_logdef check_gateway(): ... |
def add_log(func):
def wrapper():
print(f"開始:{func.__name__}")
result = func()
print(f"結束:{func.__name__}")
return result
return wrapper
@add_log
def check_gateway():
return "閘道器連線正常"
print(check_gateway())@add_log只是較清楚地表示「定義函式後,立刻用add_log包裝它」。五、*args與**kwargs支援不同函式
前面的wrapper()只能包裝沒有參數的函式。通用Decorator通常使用:
*args收集位置引數。**kwargs收集關鍵字引數。
def add_log(func):
def wrapper(*args, **kwargs):
print(f"開始:{func.__name__}")
result = func(*args, **kwargs)
print(f"結束:{func.__name__}")
return result
return wrapper
@add_log
def read_sensor(pond_name, sensor_name="水溫"):
return f"讀取{pond_name}的{sensor_name}"
print(read_sensor("一號池", sensor_name="溶氧"))六、使用functools.wraps保存原函式資訊
包裝後若查看__name__,可能只看到wrapper。使用@wraps(func)可保留原函式名稱、說明文件及其他中繼資料。
from functools import wraps
def add_log(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"開始:{func.__name__}")
return func(*args, **kwargs)
return wrapper
@add_log
def read_temperature(pond_name):
"""讀取指定養殖池的水溫。"""
return f"{pond_name}讀取完成"
print(read_temperature.__name__)
print(read_temperature.__doc__)@wraps(func)。七、加入時間與執行耗時
操作紀錄通常需要「何時開始」與「花了多久」。牆上時間可用datetime,量測耗時則適合使用time.perf_counter()。
from datetime import datetime
from functools import wraps
from time import perf_counter
def timed_log(func):
@wraps(func)
def wrapper(*args, **kwargs):
started_at = datetime.now().astimezone()
start = perf_counter()
result = func(*args, **kwargs)
elapsed = perf_counter() - start
print(f"時間:{started_at.isoformat(timespec='seconds')}")
print(f"功能:{func.__name__}")
print(f"耗時:{elapsed:.6f}秒")
return result
return wrapper
@timed_log
def calculate_energy(power, hours):
return power * hours
print("估計用電:", calculate_energy(0.23, 10), "kWh")datetime.now().astimezone()會附上本機時區偏移。跨系統集中紀錄時,可統一保存UTC,再於介面轉成臺灣時間。八、成功與失敗都要留下紀錄
Decorator可以統一捕捉錯誤、記錄後再重新拋出,讓上層程式決定如何處理。
from functools import wraps
from time import perf_counter
def operation_log(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = perf_counter()
try:
result = func(*args, **kwargs)
except Exception as error:
elapsed = perf_counter() - start
print({
"功能": func.__name__,
"狀態": "失敗",
"錯誤類型": type(error).__name__,
"耗時秒": round(elapsed, 6)
})
raise
else:
elapsed = perf_counter() - start
print({
"功能": func.__name__,
"狀態": "成功",
"耗時秒": round(elapsed, 6)
})
return result
return wrapper
@operation_log
def calculate_average(total, count):
return total / count
print(calculate_average(84, 3))九、Decorator本身也可以接收設定
若希望不同功能帶上「感測、網站、故事機」等類別,可再增加一層函式:
from functools import wraps
def categorized_log(category):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{category}] 執行{func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@categorized_log("智慧養殖")
def read_oxygen(pond_name):
return f"{pond_name}溶氧資料讀取完成"
print(read_oxygen("二號池"))@categorized_log("智慧養殖")會先取得真正的Decorator,再用它包裝函式。
十、敏感資料遮罩
通用Decorator若直接印出所有args與kwargs,可能把Token、密碼或個資寫進紀錄。以下只示範遮罩關鍵字參數:
SENSITIVE_KEYS = {"password", "token", "api_key", "電話", "身分證"}
def mask_kwargs(kwargs):
safe = {}
for key, value in kwargs.items():
if key.lower() in SENSITIVE_KEYS or key in SENSITIVE_KEYS:
safe[key] = "***"
else:
safe[key] = value
return safe
example = {
"pond_name": "一號池",
"token": "secret-token",
"電話": "0912-345-678"
}
print(mask_kwargs(example))十一、完整示範:水井USR操作稽核Decorator
from datetime import datetime, timezone
from functools import wraps
from time import perf_counter
audit_records = []
def audit(category):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = perf_counter()
record = {
"時間UTC": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"類別": category,
"功能": func.__name__
}
try:
result = func(*args, **kwargs)
except Exception as error:
record["狀態"] = "失敗"
record["錯誤類型"] = type(error).__name__
raise
else:
record["狀態"] = "成功"
return result
finally:
record["耗時秒"] = round(perf_counter() - start, 6)
audit_records.append(record)
return wrapper
return decorator
@audit("智慧養殖")
def estimate_daily_energy(power, hours):
if power < 0 or hours < 0:
raise ValueError("功率與時數不得為負數")
return power * hours
print(estimate_daily_energy(0.23, 10))
print(audit_records)十二、多個Decorator的執行順序
@decorator_a
@decorator_b
def task():
pass等同於task = decorator_a(decorator_b(task))。呼叫時通常先進入外層A,再進入B,完成後由B返回A。堆疊過多會讓除錯與例外流程難以理解,應保持用途單純。
十三、Decorator適合與不適合的情況
| 適合 | 不宜濫用 |
|---|---|
| 紀錄、計時、權限檢查、快取、重試等共同功能 | 只有一個函式使用的簡單流程 |
| 希望核心函式專注業務邏輯 | Decorator會偷偷大幅改變回傳型別或副作用 |
| 規則可清楚命名及獨立測試 | 多層堆疊使執行順序難以追查 |
十四、常見錯誤
| 錯誤 | 原因 | 修正 |
|---|---|---|
| 原函式回傳值消失 | wrapper沒有return result | 把結果回傳 |
| 只能包裝無參數函式 | wrapper未接收參數 | 使用*args, **kwargs |
| 函式名稱變成wrapper | 未使用wraps | 加入@wraps(func) |
| 錯誤被悄悄吞掉 | except後沒有raise或替代流程 | 記錄後重新拋出 |
| 紀錄含Token或個資 | 直接保存所有參數 | 採白名單與遮罩,減少蒐集 |
| 紀錄只存在記憶體 | 程式重啟即消失 | 正式系統使用logging、檔案或資料庫 |
十五、漸進式練習
基礎:開始與結束
建立Decorator,在函式前後顯示開始、結束及函式名稱,並確認原回傳值沒有消失。
進階:計時與錯誤
加入perf_counter()及try-except-else-finally,分別測試成功與除以零失敗。
USR挑戰:操作紀錄欄位治理
列出故事機、養殖監測與產銷平台各自「需要記錄、禁止記錄、保存期限、可查閱角色」四項規則,再據此設計Decorator。先決定資料治理,才決定程式欄位。
十六、學習檢核
- Decorator的輸入與輸出通常是什麼?
@add_log等同哪一行手動包裝程式?- 為什麼wrapper需要
*args與**kwargs? @wraps(func)保存哪些重要資訊?- 為何記錄例外後通常還要
raise? - 為什麼不應把所有函式參數直接寫入操作紀錄?
- 多個Decorator的套用與執行順序如何理解?
十七、AI協作提示詞
請擔任Python Decorator與資料治理助教。
我要為USR系統的函式加入操作紀錄。
請先詢問:紀錄目的、允許欄位、禁止欄位、保存期限、
使用者角色、成功與失敗處理,以及是否需要計時。
請先提供手動包裝版本,再改成Decorator;
必須使用functools.wraps、*args、**kwargs,
並保留原函式回傳值與例外行為。
預設不要記錄原始參數及回傳內容,
不得保存密碼、Token、API金鑰或可識別居民個資。十八、延伸閱讀
十九、本篇小結
Decorator能把紀錄、計時與錯誤追蹤等共同功能,從核心業務函式中分離。真正可靠的操作紀錄還需要欄位白名單、敏感資料保護、時區、保存期限、權限及持久化設計。
好的操作紀錄幫助團隊追查系統,不應反過來成為居民個資與場域機密的風險。
沒有留言:
張貼留言