Python 實用程式設計技巧(裝飾器篇)
比如說,我們想為很多不同的函式新增相同的功能,比如說計時統計、記錄日誌、快取運算結果等,但是我們又不想在每個函式中新增相同的程式碼
2.舉個例子:
我們還是以斐波那契數的計算為例
示例程式碼:
def fib(n): if n <= 1: return 1 return fib(n-1)+fib(n-2) if __name__ == '__main__': print fib(50)
這一段程式碼想要跑出來非常的慢,因為我們在這個運算的過程中經歷了非常多的重複運算,比如我們想計算50就要計算49,48 我們要計算49 就要計算 48,47 ,看到了吧,48 就出現了重複運算,那麼這裡面有著太多太多的重複運算,導致我們的計算非常的慢,並且非常的消耗 CPU
那麼怎麼辦呢?
我們可以創造一個快取,每次算到一個新的結果我們都放在這個快取中,這樣我們每次都判斷快取有沒有我們想要的值就可以了,有的話直接拿過來用,沒有再加入快取,這樣就能大大提高我們的執行效率,並且減輕了我們的CPU 的負擔
示例程式碼:
def fib(n,cache = None): if cache is None: cache = {} if n in cache: return cache[n] if n <= 1: return 1 cache[n] = fib(n-1,cache)+fib(n-2,cache) return cache[n] if __name__ == '__main__': print fib(50)
結果:
我們發現效率出現了質的飛躍,很快就算出了結果,但是換做別的函式我麼又要新增這個快取的程式碼了,這樣就非常的煩,
那麼怎麼解決呢?
我們考慮建立一個包裹函式 wrap ,在這個函式內部我們實現我們的快取程式碼,並且呼叫原函式,我們的函式裝飾器就是為了生成這樣的包裹函式的
示例程式碼:
def memo(func): cache = {} def wrap(*args): if args not in cache: cache[args] = func(*args) return cache[args] return wrap def fib(n): if n <= 1: return 1 return fib(n - 1) + fib(n - 2) if __name__ == '__main__': fib = memo(fib) print fib(50)
結果:
當然這樣在函式中寫是非常囉嗦的,我們python 給我們提供了一個語法糖,
示例程式碼:
def memo(func): cache = {} def wrap(*args): if args not in cache: cache[args] = func(*args) return cache[args] return wrap @memo def fib(n): if n <= 1: return 1 return fib(n - 1) + fib(n - 2) if __name__ == '__main__': print fib(50)
結果:
二、如何為被裝飾的函式儲存元資料
1.概念:
在函式物件中儲存著一些函式的元資料,例如:
f.__name__
函式的名字
f.__doc__
函式的文件字串
f.__model__
函式所屬的模組名
f.__dict__
屬性字典
f.__defaults__
預設引數元組
我們在使用裝飾器以後,再使用上面這些屬性訪問的時候,看到的是包裹函式的元資料,而原始函式的元資料不見了,我們該如何解決
2.舉個例子:
下面是原始函式的返回結果
示例程式碼:
def example(): '''example function''' print "In example" if __name__ == '__main__': print example.__name__ print example.__doc__
結果:
example example function
這下面是經過裝飾器裝飾以後的返回結果
示例程式碼:
def mydecorator(func): def wrap(*args,**kargs): '''wrap function''' print "In wrapper" func(*args,**kargs) return wrap @mydecorator def example(): '''example function''' print "In example" if __name__ == '__main__': print example.__name__ print example.__doc__
結果:
wrap wrap function
解決:
我們使用 functools 中的 wraps 裝飾內部的包裹函式,可以定義將原函式的某些屬性更新到包裹函式上面
示例程式碼:
from functools import wraps def mydecorator(func): @wraps(func) def wrap(*args,**kargs): '''wrap function''' print "In wrapper" func(*args,**kargs) return wrap @mydecorator def example(): '''example function''' print "In example" if __name__ == '__main__': print example.__name__ print example.__doc__
結果:
example example function
三、如何自定義帶引數的裝飾器
比如說我們想實現一個裝飾器來檢查被裝飾函式的引數型別,裝飾器能定義函式的引數型別,如果函式呼叫的時引數型別不對就丟擲異常
帶引數的裝飾器就是根據引數定製化一個裝飾器,可以看成是生產裝飾器的工廠,每次呼叫這個裝飾器都能返回一個特定的裝飾器,然後再用其修飾其它函式
示例程式碼:
from inspect import signature def typeassert(*ty_args,**ty_kargs): def decorator(func): sig = signature(func) btypes = sig.bind_partial(*ty_args,**ty_kargs).arguments def wrapper(*args,**kargs): for name,obj in sig.bind(*args,**kargs).arguments.items(): if name in btypes: if not isinstance(obj,btypes[name]): raise TypeError("%s must be %s" % (name,btypes[name])) return func(*args,**kargs) return wrapper return decorator @typeassert(int,str,list) def f(a,b,c): print (a,b,c) if __name__ == '__main__': f(1,"abc",[1,2,3]) f(1,2,[1,2,3])
結果:
1 abc [1, 2, 3] TypeError: b must be <class 'str'>
四、如何實現屬性可修改的裝飾器
背景:
為了分析程式內哪些程式開銷較大,我們可以定義一個帶有timeout 引數的函式裝飾器,他實現以下功能:
1.統計被裝飾的函式的單次呼叫的時間
2.時間大於timeout 的將此次函式的呼叫記錄記錄在 log 日誌檔案中
3.執行時可以修改 timeout 的值
示例程式碼:
from functools import wraps import time import logging from random import randint def warn(timeout): def decorator(func): @wraps(func) def wrapper(*args,**kargs): start = time.time() res = func(*args,**kargs) used = time.time() - start if used > timeout: msg = "%s : %s > %s" % (func.__name__,used,timeout) logging.warn(msg) return res return wrapper return decorator @warn(1.5) def test(): print("In test") while randint(0,1): time.sleep(0.5) for i in range(30): test()
我們可以在包裹中新增一個函式,然後用這個函式來修改閉包中的自由變數
Python3
示例程式碼:
from functools import wraps import time import logging from random import randint def warn(timeout): def decorator(func): @wraps(func) def wrapper(*args,**kargs): start = time.time() res = func(*args,**kargs) used = time.time() - start if used > timeout: msg = "%s : %s > %s" % (func.__name__,used,timeout) logging.warn(msg) return res def setTimeout(k): nonlocal timeout timeout = k wrapper.setTimeout = setTimeout return wrapper return decorator @warn(1) def test(): print("In test") while randint(0,1): time.sleep(0.5) for i in range(30): test() test.setTimeout(1) for i in range(30): test()
但是由於 python2 並不支援 nonlocal ,於是我們還要修改,使用列表將其修改成一個可變變數
示例程式碼:
from functools import wraps import time import logging from random import randint def warn(timeout): timeout = [timeout] def decorator(func): @wraps(func) def wrapper(*args,**kargs): start = time.time() res = func(*args,**kargs) used = time.time() - start if used > timeout[0]: msg = "%s : %s > %s" % (func.__name__,used,timeout[0]) logging.warn(msg) return res def setTimeout(k): #nonlocal timeout timeout[0] = k wrapper.setTimeout = setTimeout return wrapper return decorator @warn(1) def test(): print("In test") while randint(0,1): time.sleep(0.5) for i in range(30): test() test.setTimeout(1) for i in range(30): test()