Рубрики
Без рубрики

Python 裝飾器 (декоратор)

現在 的 Python 程式 會 看到 以 @ 開頭 的 程式, 有些時候 看 起來 玄 玄, 不過 太 多 地方 會 用 到 這樣 語法 語法, 因此 即使 不 去 都 不 行, 以下 來 來 一下 的 裝飾器 瞭解 不 行 以下 來 認識 所謂 『裝飾器 裝飾器 行 以下 所謂 所謂 』。 … Tagged с Python, декоратор.

現在 的 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”