顯示具有 Decorator 標籤的文章。 顯示所有文章
顯示具有 Decorator 標籤的文章。 顯示所有文章

2026年8月31日 星期一

Python一下:用Decorator記錄水井USR設備操作歷程

Python一下|第9篇

用Decorator記錄水井USR設備操作歷程

《從風土資料到智慧生活的程式設計》——不改動核心函式,也能一致加入紀錄、計時與錯誤追蹤。

本篇學習目標
  • 理解Decorator是「接收函式並回傳新函式」的高階函式。
  • 從手動包裝推導@decorator語法。
  • 使用*args**kwargs包裝不同參數的函式。
  • 使用functools.wraps保存原函式資訊。
  • 記錄操作時間、函式名稱、結果及錯誤。
  • 避免在紀錄中暴露密碼、Token、居民個資與敏感場域資訊。

一、為什麼設備操作需要紀錄?

水井USR的智慧養殖、魚菜共生、故事機與網站服務都可能包含設備操作。當程式執行「讀取感測器、切換模式、更新資料」時,若能留下適當紀錄,就較容易回答:

何時

操作與錯誤發生時間

做了什麼

呼叫哪個功能與結果

是否成功

耗時、失敗類型及追查線索

如果每個函式都自行撰寫相同的紀錄程式,會大量重複。Decorator能把這類「橫跨多個功能的共同工作」集中管理。

紀錄不等於監控一切:紀錄必須符合目的與最小必要原則。不要保存密碼、API金鑰、Token、身分證號、完整電話、居民故事原文或可識別個人的敏感資料。

二、先看沒有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_log
def 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())
Decorator不會憑空新增能力:@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__)
實務習慣:撰寫Decorator時,幾乎都應在wrapper上使用@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))
為什麼要再raise?紀錄錯誤後若完全吞掉例外,上層程式可能誤以為操作成功。除非已有明確的復原策略,通常應重新拋出。

九、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若直接印出所有argskwargs,可能把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))
遮罩仍不是萬靈丹:敏感資料可能藏在位置引數、巢狀Dict、錯誤訊息或回傳內容。最安全的做法是採白名單,只記錄明確允許的欄位,而不是先收集全部再遮罩。

十一、完整示範:水井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。先決定資料治理,才決定程式欄位。

十六、學習檢核

  1. Decorator的輸入與輸出通常是什麼?
  2. @add_log等同哪一行手動包裝程式?
  3. 為什麼wrapper需要*args**kwargs
  4. @wraps(func)保存哪些重要資訊?
  5. 為何記錄例外後通常還要raise
  6. 為什麼不應把所有函式參數直接寫入操作紀錄?
  7. 多個Decorator的套用與執行順序如何理解?

十七、AI協作提示詞

請擔任Python Decorator與資料治理助教。
我要為USR系統的函式加入操作紀錄。
請先詢問:紀錄目的、允許欄位、禁止欄位、保存期限、
使用者角色、成功與失敗處理,以及是否需要計時。

請先提供手動包裝版本,再改成Decorator;
必須使用functools.wraps、*args、**kwargs,
並保留原函式回傳值與例外行為。
預設不要記錄原始參數及回傳內容,
不得保存密碼、Token、API金鑰或可識別居民個資。

十八、延伸閱讀

十九、本篇小結

Decorator能把紀錄、計時與錯誤追蹤等共同功能,從核心業務函式中分離。真正可靠的操作紀錄還需要欄位白名單、敏感資料保護、時區、保存期限、權限及持久化設計。

好的操作紀錄幫助團隊追查系統,不應反過來成為居民個資與場域機密的風險。