目錄

廣告 AD

掌握 Python Decorators:從基礎到進階的各種寫法

因為工作需求對 decorator 研究了一下

將結果整理紀錄在這邊

廣告 AD

Decorator

我們都知道 decorator 的作用就是裝飾,對我們想要的 function 多增加一層包裝

有點類似下面的關係,將 func 替換成經過 decorator 包裝的 func

python

func = decorator(func)

接下來我們一共介紹 4 種不同使用的方式,可以依照需求選擇


我打算寫一個幫我們紀錄 function 執行所花費時間的 decorator

我們會先建立一個叫做 print_elapsed_time 的 function,這個 function 會回傳另一個 function (wrapper)

wrapper 裡面會記錄開始的時間和結束的時間,中間呼叫 print_elapsed_time 傳進來的參數 func,也就是被裝飾的 function

最後在 func 執行完後印出所花費的時間

python

def print_elapsed_time(func):
    def wrapper(*arg, **kwarg):
        start = timer()
        r = func(*arg, **kwarg)
        end = timer()
        print(f"Elapsed time: {end - start:.3f} seconds")
        return r
    return wrapper

最後用 print_elapsed_time 這個 decorator 來裝飾 sleep_t_sec 這個 function

python

@print_elapsed_time
def sleep_t_sec(t:int):
    time.sleep(t)

sleep_t_sec(3)

完整範例:

python

from timeit import default_timer as timer
import time

def print_elapsed_time(func):
    def wrapper(*arg, **kwarg):
        start = timer()
        r = func(*arg, **kwarg)
        end = timer()
        print(f"Elapsed time: {end - start:.3f} seconds")
        return r
    return wrapper
    
@print_elapsed_time
def sleep_t_sec(t:int):
    time.sleep(t)
    
sleep_t_sec(3)

與上面的類似,只是透過 class 來模擬外層 function,最後還是回傳另一個 wrapper function

要做到與 function 一樣可以呼叫,我們就要在 class 內實作 __call__ 的 function

__call__ 的 function 裡面我們回傳與上面方法一樣的 wrapper function

python

class PrintElapsedTime():
    def __call__(self, func):
        def wrapper(*arg, **kwarg):
            start = timer()
            r = func(*arg, **kwarg)
            end = timer()
            print(f"Elapsed time: {end - start:.3f} seconds")
            return r
        return wrapper

與上面方法類似,只是多了一個 ()

python

@PrintElapsedTime()
def sleep_t_sec(t:int):
    time.sleep(t)
    
sleep_t_sec(3)

完整範例:

python

from timeit import default_timer as timer
import time

class PrintElapsedTime():
    def __call__(self, func):
        def wrapper(*arg, **kwarg):
            start = timer()
            r = func(*arg, **kwarg)
            end = timer()
            print(f"Elapsed time: {end - start:.3f} seconds")
            return r
        return wrapper
    
@PrintElapsedTime()
def sleep_t_sec(t:int):
    time.sleep(t)
    
sleep_t_sec(3)

如果你希望要透過 class 來製作 decorator,其實也可以

透過 contextlib 的 ContextDecorator,我們可以將 class 當作 decorator

首先我們可以將 class 繼承 ContextDecorator,並實作 __enter____exit__

__enter__ 代表在執行前要需要做的事情,__exit__ 則代表在離開前要做的事情

所以我們分別在 __enter__ 紀錄開始時間,在 __exit__ 紀錄結束時間,並印出花費時間

python

class PrintElapsedTime(ContextDecorator):
    def __enter__(self):
        self.start = timer()
        return self

    def __exit__(self, *exc):
        end = timer()
        print(f"Elapsed time: {end - self.start:.3f} seconds")

接著依照一般的 decorator 使用即可,記得是 @PrintElapsedTime(),有多了一個 ()

python

@PrintElapsedTime()
def sleep_t_sec(t:int):
    time.sleep(t)

sleep_t_sec(3)

ContextDecorator 也同樣支援使用 with 的方法,可以針對所需要的段落的 code 做裝飾:

python

def sleep_t_sec(t:int):
    with PrintElapsedTime():
        time.sleep(t)

sleep_t_sec(3)

完整範例:

python

from timeit import default_timer as timer
from contextlib import ContextDecorator
import time

class PrintElapsedTime(ContextDecorator):
    def __enter__(self):
        self.start = timer()
        return self

    def __exit__(self, *exc):
        end = timer()
        print(f"Elapsed time: {end - self.start:.3f} seconds")

def sleep_t_sec(t:int):
    with PrintElapsedTime():
        time.sleep(t)

sleep_t_sec(3)

除了上述的方式之外,我們也可以使用 contextmanager 來達到使用 yield 來製作 decorator

這樣會讓我們在執行 decorator 的途中跳出去執行目標的 function,結束後再回來執行 decorator 剩餘的 code

如此,我們的 decorator function 內部就需要 yield,我們將 yield 放置在了紀錄開始時間和結束時間的中間

最後印出所花費的時間

python

@contextmanager
def print_elapsed_time():
    start = timer()
    yield
    end = timer()
    print(f"Elapsed time: {end - start:.3f} seconds")

使用 decorator,記得一樣需要 ()

python

@print_elapsed_time()
def sleep_t_sec(t:int):
    time.sleep(t)
    
sleep_t_sec(3)

contextmanager 也支援使用 with 來搭配使用

python

def sleep_t_sec(t:int):
    with print_elapsed_time():
        time.sleep(t)

sleep_t_sec(3)

完整範例:

python

from timeit import default_timer as timer
from contextlib import contextmanager
import time

@contextmanager
def print_elapsed_time():
    start = timer()
    yield
    end = timer()
    print(f"Elapsed time: {end - start:.3f} seconds")

def sleep_t_sec(t:int):
    with print_elapsed_time():
        time.sleep(t)

sleep_t_sec(3)


廣告 AD