有時候我們想為多個函數(shù)在张,同意添加某一種功能用含,比如及時統(tǒng)計,記錄日志帮匾,緩存運算結果等等啄骇,而又不想改變函數(shù)代碼
那就定義裝飾器函數(shù),用它來生成一個在原函數(shù)基礎添加了新功能的函數(shù)瘟斜,代替原函數(shù)
參考金角大王的博客
裝飾器從無到有的過程
比如現(xiàn)在有三個已經(jīng)實現(xiàn)功能的函數(shù)
def shoping():
print 'shoping'
def info():
print 'information'
def talking():
print 'talking'
然后然后想給每個模塊加一個登陸驗證功能
user_status = False # 先定義變量
def login():
_username = "ketchup" #假裝這是DB里存的用戶信息
_password = "123456" #假裝這是DB里存的用戶信息
global user_status
if user_status == False:
username = input("user:")
password = input("pasword:")
if username == _username and password == _password:
print("welcome login....")
user_status = True
else:
print("wrong username or password!")
else:
print("用戶已登錄缸夹,驗證通過...")
一開始的想法是這樣痪寻,在每個函數(shù)中添加函數(shù)
def shoping():
login()
print 'shoping'
def info():
login()
print 'info'
def talking():
login()
print 'talking'
但是,軟件開發(fā)要遵循‘開放封閉的原則’虽惭,它規(guī)定已經(jīng)實現(xiàn)的功能代碼不允許被修改橡类,但可以被擴展,
封閉:就是已經(jīng)實現(xiàn)功能的代碼塊芽唇,盡量不在內(nèi)部做修改
開放:對擴展開發(fā)
然后修改為:
user_status = False # 先定義變量
def login(func):
_username = "ketchup" #假裝這是DB里存的用戶信息
_password = "123456" #假裝這是DB里存的用戶信息
global user_status
if user_status == False:
username = input("user:")
password = input("pasword:")
if username == _username and password == _password:
print("welcome login....")
user_status = True
else:
print("wrong username or password!")
if user_status == True:
func() #如果登陸成功顾画,就調(diào)用傳入的函數(shù)
def shoping():
print 'shoping'
def info():
print 'info'
def talking():
print 'talking'
login(shoping) #這樣,先執(zhí)行l(wèi)ogin函數(shù)匆笤,在login函數(shù)內(nèi)部就會調(diào)用執(zhí)行shoping函數(shù)
login(info)
login(talking)
但是每次這樣調(diào)用研侣,如果很多模塊要加這個功能,大家都要去調(diào)用一下炮捧,那太麻煩了</br>
python中一切皆對象庶诡,可以用
shopping = login(shoping)
shopping()
這樣的方式執(zhí)行shopping()就等于執(zhí)行l(wèi)ogin(shopping)</br>
但是在前面賦值 shopping = login(shoping)的時候,就已經(jīng)調(diào)用login()函數(shù)了寓盗,執(zhí)行了里面的func()函數(shù)
要解決這個問題灌砖,就要在shopping = login(shoping)這次調(diào)用的時候,不執(zhí)行func函數(shù)傀蚌,只是把一個函數(shù)名給了他基显,然后下面shoppin()函數(shù)執(zhí)行的時候才會執(zhí)行,
所以善炫,就要在login函數(shù)里加一層閉包函數(shù)
def login(func):
def wrapper():
_username = "ketchup" #假裝這是DB里存的用戶信息
_password = "123456" #假裝這是DB里存的用戶信息
global user_status
if user_status == False:
username = input("user:")
password = input("pasword:")
if username == _username and password == _password:
print("welcome login....")
user_status = True
else:
print("wrong username or password!")
if user_status == True:
func() #如果登陸成功撩幽,就調(diào)用傳入的函數(shù)
return wrapper
這樣的話,第一次shopping = login(shopping) 的時候箩艺,shopping 的值為wrapper
后面
def shoping():
print 'shoping'
的時候窜醉,才執(zhí)行wrapper() ,才調(diào)用里面的func()
然后python對這種寫法有一個語法糖 這種寫法就等于在shopping函數(shù)前面加上@login
如果要在shopping里面?zhèn)鲄?shù)怎么辦呢艺谆?
那就要在login里面把參數(shù)拿過來榨惰,然后傳給func
def login(func):
def wrapper(*args,**kwargs):
_username = "ketchup" #假裝這是DB里存的用戶信息
_password = "123456" #假裝這是DB里存的用戶信息
global user_status
if user_status == False:
username = input("user:")
password = input("pasword:")
if username == _username and password == _password:
print("welcome login....")
user_status = True
else:
print("wrong username or password!")
if user_status == True:
func(*args,**kwargs) #如果登陸成功,就調(diào)用傳入的函數(shù)
return wrapper
@login
def shoping(num):
print 'shoping %d 個'%num
@login
def info():
print 'info'
@login
def talking():
print 'talking'
如果這時候要對login傳參數(shù)呢静汤,那就在多加一層對login參數(shù)的判斷琅催,比如,要判斷是從qq還是weixin登陸的
def login(auth_type):
def auth(func):
def wrapper(*args,**kwargs):
if auth_type == 'qq':
_username = "ketchup" #假裝這是DB里存的用戶信息
_password = "123456" #假裝這是DB里存的用戶信息
global user_status
if user_status == False:
username = input("user:")
password = input("pasword:")
if username == _username and password == _password:
print("welcome login....")
user_status = True
else:
print("wrong username or password!")
if user_status == True:
func(*args,**kwargs) #如果登陸成功虫给,就調(diào)用傳入的函數(shù)
else:
print('only support qq or weixin ')
return wrapper
return auth
下面以幾個實際問題的例子深入理解裝飾器
1-實現(xiàn)斐波那契的幾個方法
為什么要用裝飾器實現(xiàn)斐波那契藤抡,因為實現(xiàn)過程中有很多重復的步驟,所以這樣很浪費
def memo(func):
cache = {}
def wrap(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrap
@memo
def fib1(n):
if n<=1:
return 1
return fib1(n-1) + fib1(n-2)
def fib2(n,cache = None):
if cache is None:
cache = {}
if n in cache:
return cache[n]
if n<= 1:
return 1
cache[n] = fib2(n-1,cache) + fib2(n-2,cache)
return cache[n]
def fib3(n):
a,b = 1,1
while n >= 2:
a,b = b, a+b
n -= 1
return b
def fib4(n):
li = [1,1]
while n >=2:
li.append(li[-2]+li[-1])
n -= 1
return li[-1]
測試:
/ print(fib1(500))
print(fib2(500))
print(fib3(500))
print(fib4(500))
22559151616193633087251269503607207204601132491375819058863886641847462773868688340
5015987052796968498626
22559151616193633087251269503607207204601132491375819058863886641847462773868688340
5015987052796968498626
22559151616193633087251269503607207204601132491375819058863886641847462773868688340
5015987052796968498626
當?shù)?00 的時候抹估,fib1已經(jīng)報錯了缠黍,RecursionError: maximum recursion depth exceeded in comparison
報錯是因為每一級遞歸都需要調(diào)用函數(shù), 會創(chuàng)建新的棧,
隨著遞歸深度的增加, 創(chuàng)建的棧越來越多, 造成爆棧
當1000的時候,fib2 也報這個錯誤了
因為python 不支持尾遞歸药蜻,所以超過1000也會報錯
關于尾遞歸參照Python開啟尾遞歸優(yōu)化
下面小練習:
如果有n級臺階瓷式,每一次可以跨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(200,(1,2,3))
52622583840983769603765180599790256716084480555530641
有時候我們想為多個函數(shù),同意添加某一種功能蒿往,比如及時統(tǒng)計盛垦,記錄日志,緩存運算結果等等
而又不想改變函數(shù)代碼
定義裝飾器函數(shù)瓤漏,用它來生成一個在原函數(shù)基礎添加了新功能的函數(shù)腾夯,代替原函數(shù)
2-為被裝飾的函數(shù)保留原來的元數(shù)據(jù)
解決方法:
使用標準庫functools中的裝飾器wraps裝飾內(nèi)部包裹函數(shù),可以定制原函>數(shù)的某些屬性蔬充,更新到包裹函數(shù)上面
先看一下函數(shù)的元數(shù)據(jù)一般有哪些:
f.name : 函數(shù)的名字</br>
f.doc : 函數(shù)的文檔字符串蝶俱,對這個函數(shù)的一些描述</br>
f.moudle : 函數(shù)所屬的模塊名</br>
f.dict : 屬性字典</br>
f.defaults : 默認參數(shù)元祖</br>
In [1]: def f():
...: '''f doc'''
...: print('ffff')
...:
In [11]: f.__doc__
Out[11]: 'f doc'
In [12]: f.__module__
Out[12]: '__main__'
In [13]: f.__defaults__
In [14]: def f(a, b=1, c=[]):
...: print a,b,c
...:
In [15]: f.__defaults__
Out[15]: (1, [])
In [17]: f.__defaults__[1].append('abc')
In [19]: f(5)
5 1 ['abc']
所以在默認參數(shù)里盡量不要使用可變對象
In [20]: f.__closure__
In [21]: def f():
...: a = 2
...: return lambda k:a**k
...:
In [22]: g = f()
In [27]: g.__closure__
Out[27]: (<cell at 0x11013af68: int object at 0x7ff0b05056e0>,)
In [28]: c = g.__closure__[0]
In [29]: c
Out[29]: <cell at 0x11013af68: int object at 0x7ff0b05056e0>
In [30]: c.cell_contents
Out[30]: 2
問題:
我們在使用裝飾器裝飾函數(shù)了之后,查看函數(shù)的元數(shù)據(jù)會顯示是裝飾器函數(shù)的元數(shù)據(jù)饥漫,而不是原函數(shù)的
def mydecorator(func):
def wrapper(*args,**kwargs):
'''wrapper function'''
print 'in wrapper'
return wrapper
@mydecorator
def example():
'''example function'''
print 'in example'
print example.__name__
print example.__doc__
運行結果:
wrapper</br>
wrapper function
解決1:
def mydecorator(func):
def wrapper(*args,**kwargs):
'''wrapper function'''
wrapper.__name__ = func.__name__
print 'in wrapper'
return wrapper
但是代碼很不優(yōu)雅
解決2:
from functools import update_wrapper
def mydecorator(func):
def wrapper(*args,**kwargs):
'''wrapper function'''
update_wrapper(wrapper, func, ('__name__', '__doc__'), ('__dic__'))
print 'in wrapper'
return wrapper
functools 里有兩個默認參數(shù)榨呆,WRAPPER_ASSIGNMENTS,WRAPPER_UPDATES 其實就對應著(‘module’, 'name', 'doc'), ('dic'),)
所以可以直接不用寫,這兩個是默認帶的
解決3:
from functools import update_wrapper
def mydecorator(func):
def wrapper(*args,**kwargs):
'''wrapper function'''
update_wrapper(wrapper, func)
print 'in wrapper'
return wrapper
最后來說這個wraps庸队,這個wraps 就是一個邊界函數(shù)积蜻,他也是一個裝飾器,是一個帶參數(shù)的裝飾器彻消,可以直接用
使用標準庫functools中的裝飾器wraps裝飾內(nèi)部包裹函數(shù)竿拆,可以定制原函數(shù)的某些屬性,更新到包裹函數(shù)上面
解決end:
from functools import update_wrapper,wraps
def mydecorator(func):
@wraps(func)
def wrapper(*args,**kwargs):
'''wrapper function'''
#update_wrapper(wrapper, func)
print 'in wrapper'
return wrapper
3-定義帶參數(shù)的裝飾器
實現(xiàn)一個裝飾器宾尚,他用來檢查唄裝飾函數(shù)的參數(shù)類型丙笋,裝飾器可以通過參數(shù)致命函數(shù)參數(shù)的類型,調(diào)用時如果檢測出類型不匹配則拋出異常
@typeassert(str,int,int)
def f(a,b,c):
....
@typeassert(y= list)
def g(x,y):
...
解決方案煌贴, 帶參數(shù)的裝飾器也就是根據(jù)參數(shù)定制出一個裝飾器可以看成生產(chǎn)裝飾器的工廠御板,每次調(diào)用typeassert 返回一個特定的裝飾器,然后用它去裝飾其他函數(shù)
from inspect import signature
def typeassert(*ty_args, **ty_kargs):
def decorator(func):
# func -> a,b
# d = {'a': int, 'b': str}
sig = signature(func)
btypes = sig.bind_partial(*ty_args, **ty_kargs).arguments
def wrapper(*args, **kargs):
# arg in d, instance(arg, d[arg])
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)
測試
f(1,'666',[1,2,3])
f(1,2,[])
TypeError: 'b' must be '<class 'str'
我們來看看signature 是怎么用的
In [1]: from inspect import signature
In [2]: def f(a, b, c=1):pass
In [16]: c = sig.parameters
In [17]: c
Out[17]:
mappingproxy({'a': <Parameter "a">,
'b': <Parameter "b">,
'c': <Parameter "c=1">})
In [18]: c = sig.parameters['c']
In [20]: c.default
Out[20]: 1
如果想對a b c 簡歷一個類型的字典{‘a(chǎn)’:'int','b':'str','c':'list'}
In [23]: bargs = sig.bind(str,int,int)
In [24]: bargs.args
Out[24]: (str, int, int)
In [26]: bargs.arguments
Out[26]: OrderedDict([('a', str), ('b', int), ('c', int)])
In [29]: bargs.arguments['a']
Out[29]: str
但是如果sig.bind(str)
只傳了一個參數(shù)牛郑,就會報錯怠肋,如果想只穿一個參數(shù)的話,就用
sig.bind_partial(str)
例:寫一個出錯重試次數(shù)的裝飾器淹朋,可以用來處理HTTP超時等
import requests
def retry(attempt):
def decorator(fuc):
def wrapper(*args, **kw):
att = 0
while att < 3:
try:
return func(*args, **kw)
except Exception as e:
att += 1
return wrapper
return decorator
#重試次數(shù)
@retry(attempt=3)
def get_response(url):
r = resquests.get('www.baidu.com')
return r.content