Python模組 adorner 的使用示例

顾平安發表於2024-07-13

模組介紹

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')]

相關文章