1. 程式人生 > >8.裝飾器的使用及問題解決技巧

8.裝飾器的使用及問題解決技巧

一. 如何使用函式裝飾器

實際案例

某些時候我們想為多個函式, 統一新增某種功能, 比如計時統計, 記錄日誌, 快取運算結果等等。

我們不想在每個函式內 一一 新增完全相同的程式碼, 有什麼好的解決方案?

解決方案

# 使用快取, 儲存計算過的結果, 以減少遞迴次數,  避免重複計算問題
def memo(func):
    cache = {}
    def wrap(*args):
        res = cache.get(args)
        if not res:
            res = cache[args] = func(*args)
        return res

    return wrap



# [題目1] 斐波那契數列(Fibonacci sequence):
# F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2)
# 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
# 求數列第n項的值?
@memo
def fibonacci(n):
    if n <= 1:
        return 1
    return fibonacci(n-1) + fibonacci(n-2)

# fibonacci = memo(fibonacci)
print(fibonacci(50))

# [題目2] 走樓梯問題
# 有100階樓梯, 一個人每次可以邁1~3階. 一共有多少走法? 
@memo
def climb(n, steps):
    count = 0
    if n == 0:
        count = 1
    elif n > 0:
        for step in steps:
            count += climb(n-step, steps)
    return count

print(climb(100, (1,2,3)))


二. 如何為被裝飾的函式儲存元資料?

實際案例

在函式物件中儲存著一些函式的元資料, 例如:

f.__name__: 函式的名字
f.__doc__: 函式文件字串
f.__module__: 函式所屬模組名
f.__dict__: 屬性字典
f.__defaults__: 預設引數元組
...


我們在使用裝飾器後, 再訪問上面這些屬性訪問時,
看到的是內部包裹函式的元陣列, 原來函式的元資料便丟失掉了, 應該如何解決?

解決方法

  • 使用update_wrapper
  • 使用wraps
from functools import update_wrapper,  wraps

def my_decorator(func):
    @wraps(func)
    def wrap(*args, **kwargs):
        '''某功能包裹函式'''

        # 此處實現某種功能
        # ...

        return func(*args, **kwargs)
    # update_wrapper(wrap, func)
    return wrap


@my_decorator
def xxx_func(a: int, b: int) -> int:  # python3  中:int   以及->int   可以起到提示作用, 傳入引數是int   返回的是int
    '''
    xxx_func函式文件:
    ...
    '''
    pass


print(xxx_func.__name__)  #xxx_func

print(xxx_func.__doc__)
'''
xxx_func函式文件:
    ...
'''





# python中閉包的使用
def nnn(a):
    i = 0

    def f():
        nonlocal i   # 修改閉包的資料 要用 nonlocal  或者列表等可變資料結構
        i += a+i
        return i

    return f

a = nnn(1)
print(a())
print(a())
print(a())
print(a())
print(a())

'''
1
3
7
15
31
'''

三. 如何定義帶引數的裝飾器?

實際案例

實現一個裝飾器, 它用來檢查被裝飾函式的引數型別。
裝飾器可以態可以通過引數指明函式引數的型別, 呼叫時如果檢測出型別不匹配則丟擲異常。


@type_assert(str, int, int)
def f(a, b, c):
    ...
    
@type_assert(y=list)
def g(x, y):
    ...

解決方案

  • 提取函式簽名: inspect.signature()
  • 帶引數的裝飾器, 也就是根據引數定製化一個裝飾器, 可以看成生產裝飾器的工廠。 每次呼叫type_assert, 返回一個特定的裝飾器,然後用它去修飾其他函式
import inspect


def type_assert(*ty_args, **ty_kwargs):  # 帶引數的裝飾器函式, 要增加一層包裹    引數是 裝飾器的引數
    def decorator(func):
        # inspect.signature(func)  函式觀察物件, 方便後面獲取 引數-型別 字典   與   引數-值字典
        func_sig = inspect.signature(func)

        # 將裝飾器引數  組成引數-型別 字典  如  {a:int, b:str}
        bind_type = func_sig.bind_partial(*ty_args, **ty_kwargs).arguments   

        # func_sig.bind_partial      繫結部分引數可以得到 引數型別字典,  
        # 比如 引數是a=1, b='bbbb', c=2    裝飾器引數是 a=int, b=str   ,則得到{'a':int, 'b':str}
        # 如果使用 func_sig.bind    則裝飾器引數中   不能缺少  c 的型別

        def wrap(*args, **kwargs):  # 引數是func的 引數
            for name, obj in func_sig.bind(*args, **kwargs).arguments.items():  # 得到 引數-值 字典
                type_ = bind_type.get(name)  # 從 引數-型別  字典中  得到 引數 應該屬於的 型別
                if type_:
                    if not isinstance(obj, type_):
                        raise TypeError('%s must be %s' % (name, type_))
            return func(*args, **kwargs)

        return wrap

    return decorator


@type_assert(c=str)
def f(a, b, c):
    pass


f(5, 10, 5.3)

# TypeError: c must be <class 'str'>

四. 如何實現屬性可修改的裝飾器?

實際案例

在某專案中, 程式執行效率差, 為分析程式內哪些函式執行時間開銷大, 我們實現一個帶timeout引數的函式裝飾器。 裝飾功能如下:

@warn_timeout(1.5)
def func(a, b):
    ...


1.統計被裝飾函式單次呼叫執行時間 
2.時間大於引數 timeout的, 將此次函式呼叫記錄到log 日誌中
3.執行時可修改 timeout 的值

解決方法

  • 為包裹函式新增一個函式, 用來修改閉包中使用的自由變數。 在python3中:使用nonlocal 來訪問潛逃作用域中的變數引用
import time
import logging


def warn_timeout(timeout):
    def decorator(func):
        # _timeout = [timeout]
        def wrap(*args, **kwargs):
            # timeout = _timeout[0]
            t0 = time.time()
            res = func(*args, **kwargs)
            used = time.time() - t0
            if used > timeout:
                logging.warning('%s: %s > %s', func.__name__, used, timeout)  # logging.warning 列印 輸出到控制檯
            return res

        def set_timeout(new_timeout):
            nonlocal timeout    # timeout 是閉包 變數
            timeout = new_timeout
            # _timeout[0] = new_timeout

        wrap.set_timeout = set_timeout  # 使timeout 可修改
        return wrap

    return decorator


import random


@warn_timeout(1.5)
def f(i):
    print('in f [%s]' % i)
    while random.randint(0, 1):
        time.sleep(0.6)


for i in range(30):
    f(i)

f.set_timeout(1)    # 修改timeout  引數    從1.5 變為1
for i in range(30):
    f(i)

五. 如何在類中定義裝飾器?

實際案例

實現一個能將函式呼叫資訊記錄到日誌的裝飾器:
1. 把每次函式的呼叫時間, 執行時間, 呼叫次數寫入日誌
2. 可以對被裝飾函式分組, 呼叫資訊記錄到不同日誌
3. 動態修改引數, 比如日誌格式
4. 動態開啟關閉日誌輸出功能


@call_info(arg1, arg2, arg3...)
def func(a, b):
    ...

解決方案

  • 為了讓裝飾器在使用上更加靈活, 可以把類的例項方法作為裝飾器,此時在包裹函式中就可以持有例項物件, 便於修改屬性和拓展功能
import time
import logging

DEFAULT_FORMAT = '%(func_name)s -> %(call_time)s\t%(used_time)s\t%(call_n)s'


class CallInfo:
    def __init__(self, log_path, format_=DEFAULT_FORMAT, on_off=True):
        self.log = logging.getLogger(log_path)
        self.log.addHandler(logging.FileHandler(log_path))  
        # 這樣可以通過log 往  log_path 輸出資訊
        self.log.setLevel(logging.INFO)  # 設定log級別
        self.format = format_
        self.is_on = on_off

    # 裝飾器方法
    def info(self, func):
        _call_n = 0   # 被呼叫次數

        def wrap(*args, **kwargs):
            func_name = func.__name__
            call_time = time.strftime('%x %X', time.localtime())  
            # localtime 格式化時間戳為本地的時間    strftime 則得到時間字串
            # % x
            # 本地相應的日期表示
            # % X
            # 本地相應的時間表示
            t0 = time.time()
            res = func(*args, **kwargs)
            used_time = time.time() - t0
            nonlocal _call_n
            _call_n += 1
            call_n = _call_n
            if self.is_on:
                self.log.info(self.format % locals())   # locals  即wrap函式中的變數 對應的字典
            return res

        return wrap

    def set_format(self, format_):
        self.format = format_

    def turn_on_off(self, on_off):
        self.is_on = on_off


# 測試程式碼
import random

ci1 = CallInfo('mylog1.log')
ci2 = CallInfo('mylog2.log')


@ci1.info
def f():
    sleep_time = random.randint(0, 6) * 0.1
    time.sleep(sleep_time)


@ci1.info
def g():
    sleep_time = random.randint(0, 8) * 0.1
    time.sleep(sleep_time)


@ci2.info
def h():
    sleep_time = random.randint(0, 7) * 0.1
    time.sleep(sleep_time)


for _ in range(30):
    random.choice([f, g, h])()

ci1.set_format('%(func_name)s -> %(call_time)s\t%(call_n)s')  # 去掉使用時間
for _ in range(30):
    random.choice([f, g])()

mylog1.log

f -> 11/04/18 17:06:35	0.6018779277801514	1
f -> 11/04/18 17:06:35	2.7894973754882812e-05	2
g -> 11/04/18 17:06:35	0.60042405128479	1
g -> 11/04/18 17:06:36	0.30515503883361816	2
....
f -> 11/04/18 17:06:47	8
f -> 11/04/18 17:06:47	9
f -> 11/04/18 17:06:48	10
g -> 11/04/18 17:06:48	14
f -> 11/04/18 17:06:48	11
f -> 11/04/18 17:06:48	12
f -> 11/04/18 17:06:49	13
g -> 11/04/18 17:06:49	15
...

mylog2.log

h -> 11/04/18 17:06:36	0.30077385902404785	1
h -> 11/04/18 17:06:37	1.71661376953125e-05	2
h -> 11/04/18 17:06:38	0.4031031131744385	3
h -> 11/04/18 17:06:38	0.2054128646850586	4
h -> 11/04/18 17:06:39	0.704901933670044	5
h -> 11/04/18 17:06:41	0.5018999576568604	6
h -> 11/04/18 17:06:41	0.10228610038757324	7
h -> 11/04/18 17:06:42	0.5047738552093506	8
h -> 11/04/18 17:06:44	0.4032928943634033	9
h -> 11/04/18 17:06:45	0.6031460762023926	10