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

因為工作需求對 decorator 研究了一下
將結果整理紀錄在這邊
Decorator
我們都知道 decorator 的作用就是裝飾,對我們想要的 function 多增加一層包裝
有點類似下面的關係,將 func 替換成經過 decorator 包裝的 func
func = decorator(func)
接下來我們一共介紹 4 種不同使用的方式,可以依照需求選擇
基礎:傳入 function 的 decorator function
我打算寫一個幫我們紀錄 function 執行所花費時間的 decorator
我們會先建立一個叫做 print_elapsed_time 的 function,這個 function 會回傳另一個 function (wrapper)
wrapper 裡面會記錄開始的時間和結束的時間,中間呼叫 print_elapsed_time 傳進來的參數 func,也就是被裝飾的 function
最後在 func 執行完後印出所花費的時間
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
@print_elapsed_time
def sleep_t_sec(t:int):
time.sleep(t)
sleep_t_sec(3)
完整範例:
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)
基礎:傳入 function 的 decorator class
與上面的類似,只是透過 class 來模擬外層 function,最後還是回傳另一個 wrapper function
要做到與 function 一樣可以呼叫,我們就要在 class 內實作 __call__
的 function
在 __call__
的 function 裡面我們回傳與上面方法一樣的 wrapper function
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)
完整範例:
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
如果你希望要透過 class 來製作 decorator,其實也可以
透過 contextlib 的 ContextDecorator,我們可以將 class 當作 decorator
首先我們可以將 class 繼承 ContextDecorator,並實作 __enter__
和 __exit__
__enter__
代表在執行前要需要做的事情,__exit__
則代表在離開前要做的事情
所以我們分別在 __enter__
紀錄開始時間,在 __exit__
紀錄結束時間,並印出花費時間
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()
,有多了一個 ()
@PrintElapsedTime()
def sleep_t_sec(t:int):
time.sleep(t)
sleep_t_sec(3)
ContextDecorator 也同樣支援使用 with 的方法,可以針對所需要的段落的 code 做裝飾:
def sleep_t_sec(t:int):
with PrintElapsedTime():
time.sleep(t)
sleep_t_sec(3)
完整範例:
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)
進階:透過 yield 的 decorator
除了上述的方式之外,我們也可以使用 contextmanager 來達到使用 yield 來製作 decorator
這樣會讓我們在執行 decorator 的途中跳出去執行目標的 function,結束後再回來執行 decorator 剩餘的 code
如此,我們的 decorator function 內部就需要 yield,我們將 yield 放置在了紀錄開始時間和結束時間的中間
最後印出所花費的時間
@contextmanager
def print_elapsed_time():
start = timer()
yield
end = timer()
print(f"Elapsed time: {end - start:.3f} seconds")
使用 decorator,記得一樣需要 ()
@print_elapsed_time()
def sleep_t_sec(t:int):
time.sleep(t)
sleep_t_sec(3)
contextmanager 也支援使用 with 來搭配使用
def sleep_t_sec(t:int):
with print_elapsed_time():
time.sleep(t)
sleep_t_sec(3)
完整範例:
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)
Reference
- https://docs.python.org/3/library/contextlib.html
- https://stackoverflow.com/questions/7370801/how-do-i-measure-elapsed-time-in-python
如果你覺得這篇文章有用 可以考慮贊助飲料給大貓咪