現在 的 Python 程式 常常 看到 以 @
開頭 的 程式, 有些時候 起來 很 玄, 不過 因為 太 多 地方 用 到 這樣 的 語法, 因此 即使 不 去 瞭解 都 行 行 的 語法 因此 即使 的 『去 瞭解 都 行, 以下 來 認識 一下 所謂『 裝飾器 』。
裝飾器 的 緣起
def works(): total = 0 for i in range(10000): total += i print("total:", total) works()
在 測試 過程 中, 我們 高度 這 這 個 函式 效能 彰 彰, 因此 能 幫 這 個 函式 計時, 一般 的 可能 是 這樣:
import time def timing(func): print("Start...") t1 = time.perf_counter() func() t2 = time.perf_counter() print("Elapsed time(secs):", t2 - t1) def works(): total = 0 for i in range(10000): total += i print("total:", total) timing(works)
我們 加上 個 計時函式, 但 為了 使用 這個 計時函式, 所有 叫 用 用 的 都 要 改 成 成 成 成 成 成 成 外 外 如果 一 不 再 再 需要 要 修改 外 如果 有 天 不 再 需要 需要 除了 一一 修改 如果 一 不 計時 時, 就 一一 回頭 把 程式 改回 原貌。 為了 避免 這 問題, 我們 可以 這樣 做:
import time def timing(func): def wrapper(): print("Start...") t1 = time.perf_counter() func() t2 = time.perf_counter() print("Elapsed time(secs):", t2 - t1) return wrapper def works(): total = 0 for i in range(10000): total += i print("total:", total) works = timing(works) works()
上述 程式 一 個 包裝函式 包裝函式 把 原本 的 函式 包裝 起來 起來 並且 倒數 第 2 列 將 包裝 過 的 函式 重新 命名 回 被 包裝 函式 名稱。 如此一 來 等於 是 為 原始 包裝 函式。 如此一 來 就 是 為 的 的 的 的 的 是 原始 原始 函式 加上 計時 的 功能, 同時 又 可以 維持 原始 的 名稱 名稱, 程式 所有 叫 用 用 函式 的 都 不 需要 修改, 就 自動 變成 的 的。 若 不 計時 計時 的 的 自動 計時版 的 往後 若 需要 計時 計時 的 的 的 會 的 功能 時, 只要 刪除 第 2 列, 就 可以 讓 Работы 變回 的 版本。
裝飾器 (декоратор) 語法
對於 這樣 需求 需求, Python 提供 一 種 種 簡便 的 語法 蜜糖 ( синтаксис Suger) 叫做 裝飾器 (декоратор) :
import time def timing(func): def wrapper(): print("Start...") t1 = time.perf_counter() func() t2 = time.perf_counter() print("Elapsed time(secs):", t2 - t1) return wrapper @timing def works(): total = 0 for i in range(10000): total += i print("total:", total) works()
@
開頭 那 一 的 意思 就 是 將 Работает 傳入 Время 後, 再 傳回 的 物件 重新 命名 為 работает, 也 就 等於 一 個 程式 的 倒數 第 2 列。 可以 把 @Timing
視為 加上 Время 功能 意思。 利用 這樣 的 語法, 完全 不 需要 更 叫 用 用 函式 的 程式, 就 幫 所有 叫 用 Работы 函式 程式 了。。
裝飾器 的 實際 動作
裝飾器 的 實際上 會 轉成 等效 的 Питон 程式 , 例如:
@decorator_here def func: ....
會 變成 的 以下 程式:
func = decorator_here(func)
其中 的 Decorator_Here
必須 是 叫 用 用 (Callable) 的 物件, 並且 符合 必須 傳入 引數 及 傳回 可 叫用 物件 的 規範, 而 就 會 把 原始 的 繫結到 叫 用 此 得到 的 傳 傳 傳 傳 函式 名稱 叫 此 物件 的 的 的 傳 回值。
這樣 講 抽象 抽象, 後續 需要 參數 的 裝飾器 時 再 進一步 說明。
裝飾 需要 的 函式 函式
如果 要 裝飾器 的 函式 具有 參數, 那麼 可以 在 定義 函式 時 利用 *args
和 ** Kwargs
來 接收 數量 的 參數, 再 轉傳 給 被 包裝 函式, 例如:
import time def timing(func): def wrapper(*args, **kwargs): print("Start...") t1 = time.perf_counter() func(*args, **kwargs) t2 = time.perf_counter() print("Elapsed time(secs):", t2 - t1) return wrapper @timing def works(start, stop): total = 0 for i in range(start, stop): total += i print("total:", total) works(1, 10000)
這樣一來, 不管是 一 個 函式, 都 可以 套用 套用 裝飾器。 要 套用 裝飾器 的 函式 有 傳回值, 也 在 包裝 函式 中 接收 傳 後 再 轉傳 出來 即 可 可 可 可 , 例如:
import time def timing(func): def wrapper(*args, **kwargs): print("Start...") t1 = time.perf_counter() res = func(*args, **kwargs) t2 = time.perf_counter() print("Elapsed time(secs):", t2 - t1) return res return wrapper @timing def works(start, stop): total = 0 for i in range(start, stop): total += i return total print("total:", works(1, 10000))
需要 參數 的 裝飾器
假如 我們 在 每 次 執行 這個 程式 時 可以 計算 該 執行 不同 次數 的 時間 就 就 希望 可以 在 套用 時 指定 次數, 那 可以 再 用 一 函式 包裝 原本 的 裝飾器, 例如 就 再 用 層 函式 原本 的 裝飾器 例如:
import time def timing(times): def outer(func): def wrapper(): print("Start...") t1 = time.perf_counter() for i in range(times): func() t2 = time.perf_counter() print("Elapsed time(secs):", t2 - t1) return wrapper return outer @timing(2) def works(): print("running...") total = 0 for i in range(10000): total += i print("total:", works())
這裡 我們 原本 的 裝飾器 結構 上 再 加 了 層 函式 函式, 並且 叫用 裝飾器 時 傳遞 了 代表 執行 次數 參數 參數 這時 依照 的 實際 中 所 提到, 變成 以下 等 效程式 依照 的 動作 所 提到 變成 的 等 效程式 效程式 依照 的 的 提到 變成 的:
works = timing(2)(works)
這裡 Время (2)
會 傳回 внешний
函式, 而 Внешний
函式 符合 傳入 單一 引數 及 傳回 可 叫用 物件 的 規範, 因此 裝飾器語法 的 等 效程式 就 可以 正確 運作。。。。。
要 注意 的 是, 在 傳回 внешний
時, 會 將 隨 Время (2)
傳入 2 所 繫結 的 время
名稱 納入 閉包 (закрытие), 因此 後續 叫用 Внешний
時 就 可以 透過 время
取得 指定 執行 次數, 據 此 傳回 最後 的 обертка
函式。
在 Python 3.9 之前 語法 中 中, 雖然 在 裝飾器語法 中 可以 叫 用函式, 但 規定 就 只 有 一 層 的 叫用 , 例如 你 能 這樣 使用 裝飾器語法:
def deco_multiple(): def outmost(): def outer(func): def wrapper(): print('start') func() print('done') return wrapper return outer return outmost @deco_multiple()() def works(): print('working.') works()
雖然 deco_multiple ()
會 傳回 вновь
, 因此 deco_multiple () ()
就 等於 out Most ()
會 傳回 внешний
, 而 внешний
符合 必須 傳入 引數 及 傳回 可 叫 用 物件 的 規範, 但是 的 規格 就 是 @ 後面 只 能 出現 一 函式 叫 用, 因此 上面 的 程式 時 會 出現 語法 錯誤:
$ py .\deco_multi_test.py File "dec.py", line 12 @deco_multiple()() ^ SyntaxError: invalid syntax
不過 這個 規定 在 Python 3.9 中 鬆綁 鬆綁 了, 現在 你 可以 可以 可以 可以 可以 可以 使用 任何 用 在 指派 運算 右邊 的 運算式 作為 裝飾器 , 只要 該 的 結果 符合 裝飾器 的 規範 即 可。 因此 的 例子 若 在 在 Python 3.9 是 可以 運作 的:
$ py .\deco_multi_test.py start working. done
裝飾器 的 用途
裝飾器 常 實作 登記 處理函式, 例如 gui 介面 事件 處理 函式 函式, 或是 伺服器 路徑 的 處理 函式。。
使用 裝飾器 處理 事件
底下 我們 個 簡單 的 例子, 來 模擬 gui 介面 的 事件 函式:
class EventHandler: def __init__(self): self.handlers = {} def on(self, ev): def wrapper(func): self.handlers[ev] = func return func return wrapper def dispatch(self, ev): if ev in self.handlers: self.handlers[ev]()
這個 EventHandler 類別 登記 不同 名稱 事件 的 對應 處理函式, 其中 的 函式 就 是 登記 登記 處理函式, 我們 將 實 作成 裝飾器 的 形式; отправка 函式 是 會 依據 的, 叫 用 已 已 已 已 已 已 已 已 已 已 已 已 已 註冊 處理 事件 的 函式。。
接著 , 就 可以 產生 產生 類別的 類別的 物件, 並且 裝飾器 註冊 各 種 事件 的 處理 函式:
e = EventHandler() @e.on('click') def onClick(): print('clicked') @e.on('double_click') def onDoubleClick(): print('double clicked')
之後, 就 叫用 叫用 處理 個別 事件 事件:
e.dispatch('click') e.dispatch('double_click')
若 這 程式 名稱 為 deco_event.py, 執行 結果 如下:
$ python .\deco_event.py clicked double clicked
利用 裝飾器 語法 語法, 可以 清楚 的 表達 表達 表達 表達 和 和 和 和 是 為了 這 這 個 物件 所 的 事件 處理 函式, 也 明確 的 個別 處理 的 名稱。。。。。。。。。。 名稱 名稱 事件 事件
裝飾器語法 與 語法 並 存
如果 你 上述 事件 處理 的 方式 對於 不 熟悉 裝飾器語法 的 人 有點 難 懂, 也 可以 實 作成 的 變化 方式:
class EventHandler: def __init__(self): self.handlers = {} def on(self, ev, func = None): if func: self.handlers[ev] = func else: def wrapper(func): self.handlers[ev] = func return func return wrapper def dispatch(self, ev): if ev in self.handlers: self.handlers[ev]() e = EventHandler() @e.on('click') def onClick(): print('clicked') def onDoubleClick(): print('double clicked') e.on('double_click', onDoubleClick) e.dispatch('click') e.dispatch('double_click')
使用 裝飾器語法 時, 因為 用 用 on ()
時 沒有 傳遞函式 給 Func 參數, 因此 會 執行 иначе
的 部分, 也 是 原本 裝飾器 的 實作 內容; 但 若 直接 用 用 on ()
時 就 同時 傳遞 事件 處理 函式, 兩 語法 並 存 存, 都 達到 相同 的 效果。。。。
裝飾器 不 要 改變 原始函式 的 功能 功能 功能 功能
你 也 發現 到, 在 這 例子 例子 中, 雖然 裝飾器 加上 了 可以 特定 事件 的 功能 功能, 但是 我們 並 沒有 函式 的 內容 , 實際上 要 運用 裝飾器, 並 沒有 什麼 限制, 完全 看 自己 的 創意。
Оригинал: “https://dev.to/codemee/python-decorator-3bni”