模組介紹
adorner
是一個現代輕量級的 Python 裝飾器輔助模組。
目前該模組僅實現了 4 個類,對應著 4 個功能:製造裝飾器
、執行計時
、函式快取
、捕獲重試
。
倉庫地址:https://github.com/gupingan/adorner
安裝
該模組可在上方倉庫中的 Releases 頁面下載 tar.gz
檔案後離線安裝,也可以透過包管理工具進行下載安裝:
pip install adorner
也可以嘗試下方這個命令:
pip install adorner -i http://mirrors.cloud.tencent.com/pypi/simple/
或者更換為: https://mirrors.cloud.tencent.com/pypi/simple/
類 Decorator
Decorator
類用於標記裝飾器函式,使裝飾器的構造更簡單。它允許你定義一個裝飾器並將其應用到函式上,簡化了裝飾器的建立和使用過程。
原始碼註釋
class Decorator(object):
def __init__(self, decorator=None):
self.decorator = decorator or (lambda s: s.execute()) # 是傳入的裝飾器函式,如果沒有傳入,則預設使用一個簡單的 lambda 函式,該函式呼叫 self.execute()。
self.function = None # 用於儲存被裝飾的函式。
# args 和 .kwargs 分別用於儲存傳遞給被裝飾函式的位置引數和關鍵字引數。
self.args = tuple()
self.kwargs = dict()
def __call__(self, function):
"""
這裡是關鍵部分
__call__ 可使得類的例項可以像函式一樣被呼叫。
接收一個函式 function 作為引數,並返回一個 wrapper 函式。
wrapper 函式內部將被裝飾的函式及其引數儲存在 self.function、self.args 和 self.kwargs 中,然後呼叫 self.decorator(self)。
functools.wraps(function) 用於保持被裝飾函式的後設資料(如函式名和文件字串)
"""
@functools.wraps(function)
def wrapper(*args, **kwargs):
self.function = function
self.args = args
self.kwargs = kwargs
return self.decorator(self)
return wrapper
def __repr__(self):
"""
返回物件的字串表示形式,便於除錯和檢視物件資訊。根據是否有被裝飾的函式來返回不同的字串。
"""
decorator_name = self.decorator_name.lstrip('<').rstrip('>')
if self.function:
return f'<{self.__class__.__name__}: {decorator_name} To {self.function.__name__}>'
return f'<{self.__class__.__name__}: {decorator_name}>'
def execute(self, *args, **kwargs):
"""
用於執行被裝飾的函式。會使用傳入的引數(如果有)或儲存的引數來呼叫 _execute_sync 方法,該方法應該是為了以後適配更復雜的非同步裝飾器所提前編寫好的
"""
final_args = args if args else self.args
final_kwargs = kwargs if kwargs else self.kwargs
return self._execute_sync(final_args, final_kwargs)
def _execute_sync(self, args, kwargs):
"""
同步地執行被裝飾的函式,並返回其結果
"""
return self.function(*args, **kwargs)
@property
def function_name(self):
"""返回被裝飾函式的名稱"""
if self.function:
return self.function.__name__
return '<None>'
@property
def function_doc(self):
"""返回被裝飾函式的文件字串"""
if self.function:
return self.function.__doc__ or ''
return ''
@property
def decorator_name(self):
"""返回裝飾器的名稱"""
if self.decorator:
return self.decorator.__name__
return '<None>'
@property
def decorator_doc(self):
"""返回裝飾器的文件字串"""
if self.decorator:
return self.decorator.__doc__ or ''
return ''
示例用法
import time
from adorner import Decorator
@Decorator
def exception_decorator(self: Decorator):
"""
捕獲異常日誌的裝飾器
:param self: 裝飾器 Decorator 例項
:return: 被修飾函式的執行結果
"""
print(self.function_doc) # 列印被裝飾函式的文件
print(self.decorator_doc) # 列印裝飾器的文件
print(self.function_name) # 列印被裝飾函式的名稱
print(self.decorator_name) # 列印裝飾器的名稱
print(self.args) # 列印被裝飾函式的傳入的位置引數 (預設形參值不包含)
print(self.kwargs) # 列印被裝飾函式的傳入的關鍵字引數 (預設形參值不包含)
try:
result = self.execute() # 列印 1
# 執行被裝飾函式,不傳入任何引數時,表示使用預設的引數 self.args、self.kwargs
# 可覆蓋傳入引數
self.execute(value=2) # 列印 2
self.execute(3) # 列印3 並丟擲異常
return result
except Exception as e:
print(f"捕獲異常: {e}")
raise
@exception_decorator
def risky_function(value=1):
print(value)
if value == 3:
raise ValueError("出錯了")
try:
risky_function()
except ValueError:
pass # 捕獲異常: 出錯了
上述示例執行後,終端應該會輸出:
捕獲異常日誌的裝飾器
:param self: 裝飾器 Decorator 例項
:return: 被修飾函式的執行結果
risky_function
exception_decorator
()
{}
1
2
3
捕獲異常: 出錯了
類 Timer
Timer
類是 Decorator
類的一個子類,用於測量被裝飾函式的執行時間。它繼承了 Decorator
類的所有功能,並在執行函式時記錄開始和結束的時間,以計算函式的執行時長,該類屬於 Decorator
類的擴充套件使用。
原始碼註釋
class Timer(Decorator):
def __init__(self, decorator=None):
super().__init__(decorator) # 呼叫父類 Decorator 的建構函式,初始化裝飾器函式。
self.time = 0 # 用於儲存被裝飾函式的執行時間。
def execute(self, *args, **kwargs):
"""
執行被裝飾的函式,並記錄其執行時間。
使用 time.perf_counter() 記錄開始和結束的時間,計算函式執行時長,並儲存在 self.time 中。
"""
_start = time.perf_counter() # 記錄開始時間。
result = super().execute(*args, **kwargs) # 呼叫父類的 execute 方法執行被裝飾的函式。
_end = time.perf_counter() # 記錄結束時間。
self.time = _end - _start # 計算並儲存執行時間。
return result # 返回被裝飾函式的結果。
示例用法
下面是如何使用 Timer
類來裝飾一個函式,並測量其執行時間的示例:
import time
from adorner import Timer
timer = Timer() # 可裝飾多個函式,不過不太推薦(多個函式先後執行會覆蓋掉計時器的後設資料)
@timer
def my_function(a, b):
"""一個簡單的函式,用於演示 Timer 裝飾器的使用。"""
time.sleep(1) # 模擬一個耗時操作。
return a + b
result = my_function(1, 2)
print(f'Execution result: {result}')
print(f"Execution time: {timer.time} seconds")
輸出將類似於:
Execution result: 3
Execution time: 1.0067455 seconds
類 Cacher
Cacher
類是一個裝飾器類,用於管理和快取函式物件及其相關資料,函式不僅僅是函式,本身也是輕量級的快取器。
原始碼註釋
class Cacher:
hash = dict() # 用於儲存每個被裝飾函式的 Cacher 例項。
def __new__(cls, function):
"""
確保每個被裝飾的函式只有一個 Cacher 例項。
如果該函式已經有一個 Cacher 例項,則返回該例項;
否則,建立一個新的例項,並將其儲存在 hash 中。
"""
if function in cls.hash:
instance = cls.hash[function]
else:
instance = object.__new__(cls)
instance.function = function # 設定快取例項對應的函式
instance.data = dict() # 快取儲存的結構是字典
setattr(instance, '__name__', f'{cls.__name__}-{function.__name__}')
cls.hash[function] = instance
return instance
def __call__(self, *args, **kwargs):
"""
使 Cacher 例項可以像函式一樣被呼叫。
呼叫被裝飾的函式,並返回其結果。
"""
return self.function(*args, **kwargs)
def __repr__(self):
"""
返回物件的字串表示形式,便於除錯和檢視物件資訊。
"""
return f'<{self.__class__.__name__}: {self.function.__name__}>'
def __iter__(self):
"""
使 Cacher 例項可迭代,迭代快取資料。
"""
return iter(self.data)
def __contains__(self, item):
"""
判斷快取資料中是否包含指定的鍵。
"""
return item in self.data
def __add__(self, other):
"""
支援使用 + 運算子合並快取資料。
"""
if isinstance(other, self.__class__):
self.data.update(other.data)
return self
if isinstance(other, dict):
self.data.update(other)
return self
if isinstance(other, (tuple, list)):
self.data.update(dict(other))
return self
raise TypeError(f'unsupported operand type(s) for +: \'{type(self)}\' and \'{type(other)}\'')
def __sub__(self, other):
"""
支援使用 - 運算子從快取資料中刪除指定的鍵。
"""
if isinstance(other, self.__class__):
for key in other.data:
self.data.pop(key, None)
return self
if isinstance(other, dict):
for key in other:
self.data.pop(key, None)
return self
if isinstance(other, (tuple, list)):
self.pops(*other)
return self
raise TypeError(f'unsupported operand type(s) for -: \'{type(self)}\' and \'{type(other)}\'')
def items(self):
return self.data.items()
def set(self, key, value, safe=False):
"""
設定快取資料。
如果 safe 為 True,則只有在 key 不存在的情況下才設定值。
"""
if not safe:
self.data[key] = value
elif key not in self.data:
self.data[key] = value
return self.data[key]
def sets(self, **data_dict):
"""
批次設定快取資料。
"""
self.data.update(data_dict)
def get(self, key, default_value=None):
"""
獲取快取資料。
如果 key 不存在,則返回 default_value。
"""
return self.data.get(key, default_value)
@staticmethod
def _apply_filter(values, filter_function, filter_safe, filter_errors):
"""應用篩選函式"""
def safe_filter(value):
try:
return filter_function(value)
except filter_errors:
return False
filter_func = safe_filter if filter_safe else filter_function
return {key: value for key, value in values.items() if filter_func(value)}
@staticmethod
def _apply_map(values, map_function, map_safe, map_errors):
"""應用遍歷處理的函式"""
def safe_map(value_):
try:
return True, map_function(value_)
except map_errors:
return False, None
if map_safe:
new_values = {}
for key, value in values.items():
success, mapped_value = safe_map(value)
if success:
new_values[key] = mapped_value
return new_values
else:
return {key: map_function(value) for key, value in values.items()}
def gets(self, *keys, default_value=None, filter_function=None, map_function=None):
"""
批次獲取快取資料。
支援透過 filter_function 過濾值,透過 map_function 處理值。
"""
values = {key: self.data.get(key, default_value) for key in keys}
if filter_function:
filter_errors = filter_errors or (TypeError, ValueError, KeyError, IndexError)
values = self._apply_filter(values, filter_function, filter_safe, filter_errors)
if map_function:
map_errors = map_errors or (TypeError, ValueError, KeyError, IndexError)
values = self._apply_map(values, map_function, map_safe, map_errors)
return values
def pop(self, key, default_value=None):
"""
刪除並返回快取資料中的指定鍵。
如果鍵不存在,則返回 default_value。
"""
return self.data.pop(key, default_value)
def pops(self, *keys, default_value=None):
"""
批次刪除並返回快取資料中的指定鍵。
如果鍵不存在,則返回 default_value。
"""
return [self.data.pop(key, default_value) for key in keys]
使用案例
下面是如何使用 Cacher
類來裝飾函式,並操作快取資料的示例:
from adorner import Cacher
@Cacher
def example1(x):
"""計算乘積"""
return x * x
@Cacher
def example2(x):
"""計算和"""
return x + x
print(example1) # 列印:<Cacher: example>
# 正常呼叫
print(example1(4)) # 列印:16
# 列印函式的文件字串
print(example1.function_doc)
# 快取設定資料
example1.set('a', 1)
example1.set('b', 2)
example1.set('c', 3)
# example2.set('a', True)
# example2.set('b', False)
# 和上述一致
example2.sets(a=True, b=False, d='資料 d')
# 獲取快取資料
print(example1.get('a'))
print(example1.get('d', '資料不存在'))
# 檢查 d 是否在快取器 example1 中
print('d' in example1)
# 快取資料合併
new_cacher = example1 + example2
print(new_cacher.data) # 快取器的所有資料
# 列印:{'a': True, 'b': False, 'c': 3, 'd': '資料 d'}
print(list(new_cacher)) # 將快取器轉為列表,可呈現儲存的鍵
new_cacher += {'e': '合併的資料 e'}
# 迭代列印
for k, v in new_cacher.items():
print(k, v)
# 批次獲取資料
print(new_cacher.gets('a', 'b', 'z', default_value='沒有這個資料'))
print(new_cacher.gets('a', 'b', 'c', filter_function=lambda x: x > 1))
# 如果比較型別不一致,可能會發生錯誤,比如下面這個例子:
# print(new_cacher.gets('a', 'b', 'c', 'd', filter_function=lambda x: x > 1))
# 解決方式:你可以自行捕捉,但是那樣會很繁瑣,推薦使用 filter_safe 引數
print(new_cacher.gets('a', 'b', 'c', 'd', filter_function=lambda x: x > 1, filter_safe=True))
# 如果啟用了 filter_safe 引數還無法正常捕捉,請使用 filter_errors 指定異常,預設是 (TypeError, ValueError, KeyError, IndexError)
print(new_cacher.gets('a', 'b', 'c', 'd', filter_function=lambda x: x(),
filter_safe=True, filter_errors=(TypeError, ValueError, KeyError, IndexError)))
# 除了上述的 filter_function 引數,另外還有 map_function,同理也有 map_safe 以及 map_errors 引數
print(new_cacher.gets('a', 'b', 'c', map_function=lambda x: x > 1))
print(new_cacher.gets('a', 'b', 'c', 'd', map_function=lambda x: x > 1, map_safe=True))
print(new_cacher.gets('a', 'b', 'c', 'd', map_function=lambda x: x > 1, map_safe=True, map_errors=(TypeError,)))
# xxx_safe 引數的功能是當傳入的函式執行發生異常時對應的一個處理,當出現異常時,該值對應的鍵值都不應存在於結果中
# 優先順序別:正常獲取值 -> filter篩選 -> map遍歷處理 -> 返回結果
# 彈出某個值
print(new_cacher.pop('c'))
print(new_cacher.pop('c', default_value=None)) # 上面彈出了,這裡嘗試彈出一個不存在的,將返回 default_value(預設None)
print(new_cacher.pop('c') == new_cacher.pop('c', default_value=None))
print(new_cacher.data) # {'a': True, 'b': False, 'd': '資料 d', 'e': '合併的資料 e'}
# 批次彈出
print(new_cacher.pops('b', 'c', default_value='不存在'))
print(new_cacher.data) # {'a': True, 'd': '資料 d', 'e': '合併的資料 e'}
# 減法刪除
sub = new_cacher - [] # 支援減去 字典 {'a', 任意值} 以及元組 ('a',)
print(sub.data) # {'d': '資料 d', 'e': '合併的資料 e'}
print(new_cacher.data) # {'d': '資料 d', 'e': '合併的資料 e'}
類 Retryer
Retryer
類是一個裝飾器類,用於在指定異常發生時重試被裝飾函式的執行。它允許設定最大重試次數、重試間隔時間以及需要捕獲的異常型別。該類為函式新增了自動重試機制,適用於網路請求、檔案操作等可能會臨時失敗的操作。
原始碼註釋
from typing import Union, List, Type
import time
class Retryer:
def __init__(self, max_retry: Union[int] = 3, delay: Union[int] = 0, catches: List[Type[Exception]] = None):
"""
初始化 Retryer 例項。
:param max_retry: 最大重試次數,預設為 3。
:param delay: 每次重試之間的延遲時間(秒),預設為 0。
:param catches: 需要捕獲的異常型別列表,預設為空列表。
"""
self.max_retry = max_retry
self.delay = delay
self.catches = catches or []
self.exceptions = []
self.count = 0
def __call__(self, function):
"""使 Retryer 例項可作為裝飾器使用。"""
return Decorator(self.run)(function)
def run(self, decorator: Decorator):
"""執行重試邏輯。"""
_catch_exceptions = tuple(self.catches) if self.catches else Exception
self.exceptions.clear()
i = 0
while i <= self.max_retry:
self.count = i
try:
result = decorator.execute()
except _catch_exceptions as e:
self.exceptions.append(e)
i += 1
if i <= self.max_retry:
time.sleep(self.delay)
continue
else:
return result
raise self.exceptions[-1]
示例用法
下面是如何使用 Retryer
類來裝飾一個函式,並在指定異常發生時重試的示例:
import random
from adorner import Retryer
# 建立 Retryer 例項,設定捕獲的異常型別為 KeyError,當被裝飾的函式中出現該錯誤時將進行重試
retryer = Retryer(catches=[KeyError])
@retryer
def unreliable_function():
"""一個可能會丟擲異常的函式,用於演示 Retryer 裝飾器的使用"""
option = random.randint(0, 2)
if option == 0:
raise KeyError('Random KeyError')
elif option == 1:
raise ValueError('Random ValueError')
else:
return "Success"
try:
result = unreliable_function()
print(result)
except Exception as e:
print(f"Function failed after retries: {e}")
# 列印重試次數和捕獲的異常
print(f"Retry count: {retryer.count}")
print(f"Exceptions: {retryer.exceptions}")
輸出將類似於:
Success
Retry count: 0
Exceptions: []
或在發生異常時:
Function failed after retries: Random KeyError
Retry count: 3
Exceptions: [KeyError('Random KeyError'), KeyError('Random KeyError'), KeyError('Random KeyError')]