8.裝飾器的使用及問題解決技巧
阿新 • • 發佈:2018-12-01
一. 如何使用函式裝飾器
實際案例
某些時候我們想為多個函式, 統一新增某種功能, 比如計時統計, 記錄日誌, 快取運算結果等等。
我們不想在每個函式內 一一 新增完全相同的程式碼, 有什麼好的解決方案?
解決方案
# 使用快取, 儲存計算過的結果, 以減少遞迴次數, 避免重複計算問題 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