?
Python 裝飾器是個強大的工具,可幫你生成整潔、可重用和可維護的代碼。某種意義上說,會不會用裝飾器是區分新手和老鳥的重要標志。如果你不熟悉裝飾器,你可以將它們視為將函數作為輸入并在不改變其主要用途的情況下擴展其功能的函數。裝飾器可以有效提高你的工作效率并避免重復代碼。本文我整理了項目中經常用到的 12 個裝飾器,值得每一個Python開發者掌握。
01 @logger
我們從最簡單的裝飾器開始,手動實現一個可以記錄函數開始和結束的裝飾器。
被修飾函數的輸出結果如下所示:
-
some_function(args)
-
# ----- some_function: start -----
-
# some_function executing
-
# ----- some_function: end -----
要實現一個裝飾器,首先要給裝飾器起一個合適的名稱:這里我們給裝飾器起名為logger。
裝飾器本質上是一個函數,它將一個函數作為輸入并返回一個函數作為輸出。?輸出函數通常是輸入的擴展版。在我們的例子中,我們希望輸出函數用start和end語句包圍輸入函數的調用。
由于我們不知道輸入函數都帶有什么參數,我們可以使用?*args?和?**kwargs?從包裝函數傳遞它們。*args?和?**kwargs?允許傳遞任意數量的位置參數和關鍵字參數。
下面是logger裝飾器的示例代碼:
-
def logger(function):
-
? ? def wrapper(*args, **kwargs):
-
? ? ? ? print(f"----- {function.__name__}: start -----")
-
? ? ? ? output = function(*args, **kwargs)
-
? ? ? ? print(f"----- {function.__name__}: end -----")
-
? ? ? ? return output
-
? ? return wrapper
logger函數可以應用于任意函數,比如:
decorated_function = logger(some_function)
上面的語句是正確的,但Python 提供了更?Pythonic?的語法——使用 @ 修飾符。
因此更通常的寫法是:
-
@logger
-
def some_function(text):
-
? ? print(text)
-
some_function("first test")
-
# ----- some_function: start -----
-
# first test
-
# ----- some_function: end -----
-
some_function("second test")
-
# ----- some_function: start -----
-
# second test
-
# ----- some_function: end -----
02?@wraps
要了解 @wraps 的作用以及為什么需要它,讓我們將前面寫的logger裝飾器應用到一個將兩個數字相加的簡單函數中。
下面的代碼是未使用@wraps裝飾器的版本:
-
def logger(function):
-
? ? def wrapper(*args, **kwargs):
-
? ? ? ? """wrapper documentation"""
-
? ? ? ? print(f"----- {function.__name__}: start -----")
-
? ? ? ? output = function(*args, **kwargs)
-
? ? ? ? print(f"----- {function.__name__}: end -----")
-
? ? ? ? return output
-
? ? return wrapper
-
@logger
-
def add_two_numbers(a, b):
-
? ? """this function adds two numbers"""
-
? ? return a + b
如果我們用__name__?和?__doc__來查看被裝飾函數add_two_numbers的名稱和文檔,會得到如下結果
-
add_two_numbers.__name__
-
'wrapper'
-
add_two_numbers.__doc__
-
'wrapper documentation'
輸出的是wrapper函數的名稱和文檔。這是我們預期想要的結果,我們希望保留原始函數的名稱和文檔。這時@wraps裝飾器就派上用場了。
我們唯一需要做的就是給wrapper函數加上@wraps裝飾器。
-
from functools import wraps
-
def logger(function):
-
? ? @wraps(function)
-
? ? def wrapper(*args, **kwargs):
-
? ? ? ? """wrapper documentation"""
-
? ? ? ? print(f"----- {function.__name__}: start -----")
-
? ? ? ? output = function(*args, **kwargs)
-
? ? ? ? print(f"----- {function.__name__}: end -----")
-
? ? ? ? return output
-
? ? return wrapper
-
@logger
-
def add_two_numbers(a, b):
-
? ? """this function adds two numbers"""
-
? ? return a + b
再此檢查add_two_numbers函數的名稱和文檔,我們可以看到該函數的元數據。
-
add_two_numbers.__name__
-
# 'add_two_numbers'
-
add_two_numbers.__doc__
-
# 'this function adds two numbers'
03?@lru_cache
@lru_cache是Python內置裝飾器,可以通過from functools import lru_cache引入。@lru_cache的作用是緩存函數的返回值,當緩存裝滿時,使用least-recently-used(LRU)算法丟棄最少使用的值。
@lru_cache裝飾器適合用于輸入輸出不變且運行時間較長的任務,例如查詢數據庫、請求靜態頁面或一些繁重的處理。
在下面的示例中,我使用@lru_cache來修飾一個模擬某些處理的函數。然后連續多次對同一輸入應用該函數。
-
import random
-
import time
-
from functools import lru_cache
-
@lru_cache(maxsize=None)
-
def heavy_processing(n):
-
? ? sleep_time = n + random.random()
-
? ? time.sleep(sleep_time)
-
# 初次調用
-
%%time
-
heavy_processing(0)
-
# CPU times: user 363 μs, sys: 727 μs, total: 1.09 ms
-
# Wall time: 694 ms
-
# 第二次調用
-
%%time
-
heavy_processing(0)
-
# CPU times: user 4 μs, sys: 0 ns, total: 4 μs
-
# Wall time: 8.11 μs
-
# 第三次調用
-
%%time
-
heavy_processing(0)
-
# CPU times: user 5 μs, sys: 1 μs, total: 6 μs
-
# Wall time: 7.15 μs
從上面的輸出可以看到,第一次調用花費了694ms,因為執行了time.sleep()函數。后面兩次調用由于參數相同,直接返回緩存值,因此并沒有實際執行函數內容,因此非常快地得到函數返回。
04 @repeat
該裝飾器的所用是多次調用被修飾函數。這對于調試、壓力測試或自動化多個重復任務非常有用。
跟前面的裝飾器不同,@repeat接受一個輸入參數,
-
def repeat(number_of_times):
-
? ? def decorate(func):
-
? ? ? ? @wraps(func)
-
? ? ? ? def wrapper(*args, **kwargs):
-
? ? ? ? ? ? for _ in range(number_of_times):
-
? ? ? ? ? ? ? ? func(*args, **kwargs)
-
? ? ? ? return wrapper
-
? ? return decorate
上面的代碼定義了一個名為repeat的裝飾器,有一個輸入參數number_of_times。與前面的案例不同,這里需要decorate函數來傳遞被修飾函數。然后,裝飾器定義一個名為wrapper的函數來擴展被修飾函數。
-
@repeat(5)
-
def hello_world():
-
? ? print("hello world")
-
hello_world()
-
# hello world
-
# hello world
-
# hello world
-
# hello world
-
# hello world
05 @timeit
該裝飾器用來測量函數的執行時間并打印出來。這對調試和監控非常有用。
在下面的代碼片段中,@timeit裝飾器測量process_data函數的執行時間,并以秒為單位打印所用的時間。
-
import time
-
from functools import wraps
-
def timeit(func):
-
? ? @wraps(func)
-
? ? def wrapper(*args, **kwargs):
-
? ? ? ? start = time.perf_counter()
-
? ? ? ? result = func(*args, **kwargs)
-
? ? ? ? end = time.perf_counter()
-
? ? ? ? print(f'{func.__name__} took {end - start:.6f} seconds to complete')
-
? ? ? ? return result
-
? ? return wrapper
-
@timeit
-
def process_data():
-
? ? time.sleep(1)
-
process_data()
-
# process_data took 1.000012 seconds to complete
06?@retry
其工作原理如下:
-
wrapper函數啟動num_retrys次迭代的for循環。
-
將被修飾函數放到try/except塊中。每次迭代如果調用成功,則中斷循環并返回結果。否則,休眠sleep_time秒后繼續下一次迭代。
-
當for循環結束后函數調用依然不成功,則拋出異常。
示例代碼如下:
-
import random
-
import time
-
from functools import wraps
-
def retry(num_retries, exception_to_check, sleep_time=0):
-
? ? """
-
? ? 遇到異常嘗試重新執行裝飾器
-
? ? """
-
? ? def decorate(func):
-
? ? ? ? @wraps(func)
-
? ? ? ? def wrapper(*args, **kwargs):
-
? ? ? ? ? ? for i in range(1, num_retries+1):
-
? ? ? ? ? ? ? ? try:
-
? ? ? ? ? ? ? ? ? ? return func(*args, **kwargs)
-
? ? ? ? ? ? ? ? except exception_to_check as e:
-
? ? ? ? ? ? ? ? ? ? print(f"{func.__name__} raised {e.__class__.__name__}. Retrying...")
-
? ? ? ? ? ? ? ? ? ? if i < num_retries:
-
? ? ? ? ? ? ? ? ? ? ? ? time.sleep(sleep_time)
-
? ? ? ? ? ? # 嘗試多次后仍不成功則拋出異常
-
? ? ? ? ? ? raise e
-
? ? ? ? return wrapper
-
? ? return decorate
-
@retry(num_retries=3, exception_to_check=ValueError, sleep_time=1)
-
def random_value():
-
? ? value = random.randint(1, 5)
-
? ? if value == 3:
-
? ? ? ? raise ValueError("Value cannot be 3")
-
? ? return value
-
random_value()
-
# random_value raised ValueError. Retrying...
-
# 1
-
random_value()
-
# 5
07?@countcall
@countcall用于統計被修飾函數的調用次數。這里的調用次數會緩存在wraps的count屬性中。
-
from functools import wraps
-
def countcall(func):
-
? ? @wraps(func)
-
? ? def wrapper(*args, **kwargs):
-
? ? ? ? wrapper.count += 1
-
? ? ? ? result = func(*args, **kwargs)
-
? ? ? ? print(f'{func.__name__} has been called {wrapper.count} times')
-
? ? ? ? return result
-
? ? wrapper.count = 0
-
? ? return wrapper
-
@countcall
-
def process_data():
-
? ? pass
-
process_data()
-
process_data has been called 1 times
-
process_data()
-
process_data has been called 2 times
-
process_data()
-
process_data has been called 3 times
08?@rate_limited
@rate_limited裝飾器會在被修飾函數調用太頻繁時,休眠一段時間,從而限制函數的調用速度。這在模擬、爬蟲、接口調用防過載等場景下非常有用
-
import time
-
from functools import wraps
-
def rate_limited(max_per_second):
-
? ? min_interval = 1.0 / float(max_per_second)
-
? ? def decorate(func):
-
? ? ? ? last_time_called = [0.0]
-
? ? ? ? @wraps(func)
-
? ? ? ? def rate_limited_function(*args, **kargs):
-
? ? ? ? ? ? elapsed = time.perf_counter() - last_time_called[0]
-
? ? ? ? ? ? left_to_wait = min_interval - elapsed
-
? ? ? ? ? ? if left_to_wait > 0:
-
? ? ? ? ? ? ? ? time.sleep(left_to_wait)
-
? ? ? ? ? ? ret = func(*args, **kargs)
-
? ? ? ? ? ? last_time_called[0] = time.perf_counter()
-
? ? ? ? ? ? return ret
-
? ? ? ? return rate_limited_function
-
? ? return decorate
該裝飾器的工作原理是:測量自上次函數調用以來所經過的時間,并在必要時等待適當的時間,以確保不超過速率限制。其中等待時間=min_interval - elapsed,這里min_intervalue是兩次函數調用之間的最小時間間隔(以秒為單位),已用時間是自上次調用以來所用的時間。如果經過的時間小于最小間隔,則函數在再次執行之前等待left_to_wait秒。
?注意:該函數在調用之間引入了少量的時間開銷,但確保不超過速率限制。
如果不想自己手動實現,可以用第三方包,名叫ratelimit。
pip install ratelimit
使用非常簡單,只需要裝飾被調用函數即可:
-
from ratelimit import limits
-
import requests
-
FIFTEEN_MINUTES = 900
-
@limits(calls=15, period=FIFTEEN_MINUTES)
-
def call_api(url):
-
? ? response = requests.get(url)
-
? ? if response.status_code != 200:
-
? ? ? ? raise Exception('API response: {}'.format(response.status_code))
-
? ? return response
如果被裝飾函數的調用次數超過允許次數,則會拋出ratelimit.RateLimitException異常。要處理該異常可以將@sleep_and_retry裝飾器與@limits裝飾器一起使用。
-
@sleep_and_retry
-
@limits(calls=15, period=FIFTEEN_MINUTES)
-
def call_api(url):
-
? ? response = requests.get(url)
-
? ? if response.status_code != 200:
-
? ? ? ? raise Exception('API response: {}'.format(response.status_code))
-
? ? return response
這樣被裝飾函數在再次執行之前會休眠剩余時間。
09 @dataclass
Python 3.7 引入了@dataclass裝飾器,將其加入到標準庫,用于裝飾類。它主要用于存儲數據的類自動生成諸如__init__,?__repr__,?__eq__,?__lt__,__str__?等特殊函數。這樣可以減少模板代碼,并使類更加可讀和可維護。
另外,@dataclass還提供了現成的美化方法,可以清晰地表示對象,將其轉換為JSON格式,等等。
-
from dataclasses import dataclass,?
-
@dataclass
-
class Person:
-
? ? first_name: str
-
? ? last_name: str
-
? ? age: int
-
? ? job: str
-
? ? def __eq__(self, other):
-
? ? ? ? if isinstance(other, Person):
-
? ? ? ? ? ? return self.age == other.age
-
? ? ? ? return NotImplemented
-
? ? def __lt__(self, other):
-
? ? ? ? if isinstance(other, Person):
-
? ? ? ? ? ? return self.age < other.age
-
? ? ? ? return NotImplemented
-
john = Person(first_name="John",?
-
? ? ? ? ? ? ? last_name="Doe",?
-
? ? ? ? ? ? ? age=30,?
-
? ? ? ? ? ? ? job="doctor",)
-
anne = Person(first_name="Anne",?
-
? ? ? ? ? ? ? last_name="Smith",?
-
? ? ? ? ? ? ? age=40,?
-
? ? ? ? ? ? ? job="software engineer",)
-
print(john == anne)
-
# False
-
print(anne > john)
-
# True
-
asdict(anne)
-
#{'first_name': 'Anne',
-
# 'last_name': 'Smith',
-
# 'age': 40,
-
# 'job': 'software engineer'}
10 @register
如果你的Python腳本意外終止,但你仍想執行一些任務來保存你的工作、執行清理或打印消息,那么@register在這種情況下非常方便。
-
from atexit import register
-
@register
-
def terminate():
-
? ? perform_some_cleanup()
-
? ? print("Goodbye!")
-
while True:
-
? ? print("Hello")
運行上面的代碼會不斷在控制臺輸出"Hello",點擊Ctrl + C強制終止腳本運行,你會看到控制臺輸出"Goodbye",說明程序在中斷后執行了@register裝飾器裝飾的terminate()函數。
11 @property
@property裝飾器用于定義類屬性,這些屬性本質上是類實例屬性的getter、setter和deleter方法。
通過使用@property裝飾器,可以將方法定義為類屬性,并將其作為類屬性進行訪問,而無需顯式調用該方法。
如果您想在獲取或設置值時添加一些約束和驗證邏輯,使用@property裝飾器會非常方便。
下面的示例中,我們在rating屬性上定義了一個setter,對輸入執行約束(介于0和5之間)。
-
class Movie:
-
? ? def __init__(self, r):
-
? ? ? ? self._rating = r
-
? ? @property
-
? ? def rating(self):
-
? ? ? ? return self._rating
-
? ? @rating.setter
-
? ? def rating(self, r):
-
? ? ? ? if 0 <= r <= 5:
-
? ? ? ? ? ? self._rating = r
-
? ? ? ? else:
-
? ? ? ? ? ? raise ValueError("The movie rating must be between 0 and 5!")
-
batman = Movie(2.5)
-
batman.rating
-
# 2.5
-
batman.rating = 4
-
batman.rating
-
# 4
-
batman.rating = 10
-
# ---------------------------------------------------------------------------
-
# ValueError? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Traceback (most recent call last)
-
# Input In [16], in <cell line: 1>()
-
# ----> 1 batman.rating = 10
-
# Input In [11], in Movie.rating(self, r)
-
#? ? ? 12? ? ?self._rating = r
-
#? ? ? 13 else:
-
# ---> 14? ? ?raise ValueError("The movie rating must be between 0 and 5!")
-
#
-
# ValueError: The movie rating must be between 0 and 5!
12?@singledispatch
@singledispatch允許函數對不同類型的參數有不同的實現,有點像Java等面向對象語言中的函數重載。
-
from functools import singledispatch
-
@singledispatch
-
def fun(arg):
-
? ? print("Called with a single argument")
-
@fun.register(int)
-
def _(arg):
-
? ? print("Called with an integer")
-
@fun.register(list)
-
def _(arg):
-
? ? print("Called with a list")
-
fun(1)? # Prints "Called with an integer"
-
fun([1, 2, 3])? # Prints "Called with a list"
結論
裝飾器是一個重要的抽象思想,可以在不改變原始代碼的情況下擴展代碼,如緩存、自動重試、速率限制、日志記錄,或將類轉換為超級數據容器等。
裝飾器的功能遠不止于此,本文介紹的12個常用裝飾器只是拋磚引玉,當你理解了裝飾器思想和用法后,可以發揮創造力,實現各種自定義裝飾器來解決具體問題。
總結:
感謝每一個認真閱讀我文章的人!!!
作為一位過來人也是希望大家少走一些彎路,如果你不想再體驗一次學習時找不到資料,沒人解答問題,堅持幾天便放棄的感受的話,在這里我給大家分享一些自動化測試的學習資源,希望能給你前進的路上帶來幫助。
軟件測試面試文檔
我們學習必然是為了找到高薪的工作,下面這些面試題是來自阿里、騰訊、字節等一線互聯網大廠最新的面試資料,并且有字節大佬給出了權威的解答,刷完這一套面試資料相信大家都能找到滿意的工作。
?
? ? ? ? ? 視頻文檔獲取方式:
這份文檔和視頻資料,對于想從事【軟件測試】的朋友來說應該是最全面最完整的備戰倉庫,這個倉庫也陪伴我走過了最艱難的路程,希望也能幫助到你!以上均可以分享,點下方小卡片即可自行領取。