[Python]對於 Decorator 裝飾器的理解

Decorator 裝飾器

程式設計中的「函式」是為了解決程式碼的重複利用、模組化,同時也增加可讀性。而 Python 的「裝飾器」可以在不改變原本函式的功能之下,又再進一步強化 (簡化) 不同函式之間程式碼的共用。

Python 的「函式」是所謂的「一級函式」,支援「高階函式」的用法,可被當成參數來傳遞。「裝飾器」就是一個「兩層」的函式,先接收一個函式進行包裝 (處理),包裝完再回傳新的函式。就是「吃進去又吐出來」的意思!

我簡單統整出兩種裝飾器函式的寫法:

# 針對沒有輸入/輸出的函式
def DecoratorName1(callback):
    def wrapper():
        # Some Code...
        callback()
        # Some Code...
    return wrapper

# 針對有輸入/輸出的函式
def DecoratorName2(callback):
    def wrapper(*args, **kwargs):
        # Some Code...
        result = callback(*args, **kwargs)
        # Some Code...
        return result
    return wrapper

使用裝飾器的方法:

# 定義 OriginalFunction 函式,並將它套用 DecoratorName 裝飾器
@DecoratorName
def OriginalFunction():
    # Some Code...

# 執行原本的函式 OriginalFunction() 即可
OriginalFunction()

此處的 @ 是一個語法糖,相當於在定義完 OriginalFunction 之後,執行了一次:

OriginalFunction = DecoratorName(OriginalFunction)

所以後面執行的 OriginalFunction(),是被裝飾器 DecoratorName() 吃進去又吐出來的產物。

範例

我寫了一個範例,用一個「計時器」做為裝飾器,包裝一個「費波那契數列」的函數:

import time

def timer(callback):
    """計時器-裝飾器"""
    def wrapper(*args, **kwargs):
        print(callback.__name__)  # 看一下呼叫了哪個函式
        start_time = time.time()
        callback(*args, **kwargs)
        elapsed_time = (time.time() - start_time) * 1000
        print(f"\n{elapsed_time:.06f} ms")
    return wrapper

# 用 timer() 包裝 fib()
@timer
def fib(n):
    """求費波那契數列"""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a + b

# 執行 fib()
fib(1000)

# 執行結果:
# fib
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 
# 0.123456 ms

PS.如果沒加上 @timer 來包裝 fib() 函式,那麼執行 timer(fib)(1000) 也有一樣的結果。

再厲害一點,讓裝飾器也能傳入參數:

import time

def showTimer(title):
    """計時器-裝飾器工廠"""
    def timer(callback):
        def wrapper(*args, **kwargs):
            print(callback.__name__)  # 看一下呼叫了哪個函式
            start_time = time.time()
            callback(*args, **kwargs)
            elapsed_time = (time.time() - start_time) * 1000
            print(f"\n{title} {elapsed_time:.06f} ms")
        return wrapper
    return timer

# 用 showTimer() 包裝 fib(),並於修飾器傳入參數 
@showTimer("Elapsed Time:")
def fib(n):
    """求費波那契數列"""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a + b

# 執行 fib()
fib(1000)

# 執行結果:
# fib
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 
# Elapsed Time: 0.123456 ms

PS.如果沒加上 @showTimer() 來包裝 fib() 函式,那麼執行 showTimer("Elapsed Time:")(fib)(1000) 也有一樣的結果。

從程式碼可以看出,裝飾器函式由兩層變成了三層,這種做法稱為「Decorator Factory (裝飾器工廠)」。每多加一項功能又要多包一層,像洋蔥一樣。

如果不用 Decorator 裝飾器?

從裝飾器的原理來看,跟我寫一個函式,在函式裡呼叫另一個函式也是一樣的效果。或是在呼叫函式時,直接傳入另一個函式的執行結果,EX: func3(func1(), func2())

像前面的程式,我一樣可以用兩個函式來處理:

import time

def showTimerFunction(title, callback, *args, **kwargs):
    """計時器-函式"""
    print(callback.__name__)  # 看一下呼叫了哪個函式
    start_time = time.time()
    callback(*args, **kwargs)
    elapsed_time = (time.time() - start_time) * 1000
    print(f"\n{title} {elapsed_time:.06f} ms")

def fib(n):
    """求費波那契數列"""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a + b

# 執行 showTimerFunction() 去呼叫 fib()
showTimerFunction("Elapsed Time:", fib, 1000)

# 執行結果:
# fib
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 
# Elapsed Time: 0.123456 ms

回歸原本函式呼叫函式的作法,這樣其實更不容易迷惑,要執行哪個函式還更有彈性。我可以決定要執行原本的 fib() 或是 showTimerFuncion(),而非只能執行被 showTimer() 包裝後的 fib()

函式也不用一層一層的回傳自己。

如果不用傳遞參數,那函式寫起來還更簡單。

結論

目前還沒找到非得使用裝飾器的場景!

參考網頁

  1. No comments yet.

  1. No trackbacks yet.

return top

%d 位部落客按了讚: