一、裝飾器概述
裝飾器(無參):
- 它是一個函數(shù)
- 函數(shù)作為它的形參
- 返回值也是一個函數(shù)
- 可以使用@function_name方式,簡化調(diào)用
此處定義不準(zhǔn)確,只是方便理解
裝飾器和高階函數(shù):裝飾器是高階函數(shù),但裝飾器是對傳入函數(shù)的功能的裝飾(功能增強)
帶參裝飾器:
- 它是一個函數(shù)
- 函數(shù)作為它的形參
- 返回值是一個不帶參的裝飾器函數(shù)
- 使用@function_name(參數(shù)列表)方式調(diào)用
- 可以看做在裝飾器外層又加了一層函數(shù)
二、為什么需要裝飾器
2.1 在不是用裝飾器的情況下,給某個函數(shù)添加功能
來看一個需求:一個加法函數(shù),想增強它的功能,能夠輸出被調(diào)用過以及調(diào)用的參數(shù)信息。
原函數(shù):
def add(x, y):
return x + y
增加信息輸出功能:
def add(x, y):
print("call add, x + y") # 日志輸出到控制臺
return x + y
上面的加法函數(shù)是完成了需求,但是有以下的缺點:
- 打印語句的耦合太高,換句話說,我們不推薦去修改初始的add函數(shù)原始代碼。
- 加法函數(shù)屬于業(yè)務(wù)功能,而輸出信息的功能,屬于非業(yè)務(wù)功能代碼,不該放在業(yè)務(wù)函數(shù)加法中
2.2 使用高階函數(shù)給某個函數(shù)添加功能
def add(x,y):
return x + y
def logger(func):
print('begin') # 增強的輸出
f = func(4,5)
print('end') # 增強的功能
return f
print(logger(add))
上面的代碼做到了業(yè)務(wù)代碼與功能代碼分離,但是func函數(shù)的傳參是個問題
為了解決傳參的問題,進一步改變代碼:
def add(x,y):
return x + y
def logger(func,*args,**kwargs):
print('begin') # 增強的輸出
f = func(*args,**kwargs)
print('end') # 增強的功能
return f
print(logger(add,5,y=60))
2.3 柯里化實現(xiàn)add函數(shù)功能增強
def add(x,y):
return x + y
def logger(fn):
def wrapper(*args,**kwargs):
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper
# print(logger(add)(5,y=50)) #這行代碼等價于下面兩行代碼,只是換了一種寫法而已
add = logger(add)
print(add(x=5, y=10))
2.4 裝飾器語法糖
def logger(fn):
def wrapper(*args,**kwargs):
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper
@logger # 等價于add = logger(add),這就是裝飾器語法
def add(x,y):
return x + y
print(add(45,40))
@logger 就是裝飾器語法,本質(zhì)是柯里化實現(xiàn)函數(shù)功能增強。
三、文檔字符串
查看Python的幫助文檔:
help(function)-
文檔字符串Documentation Strings:幫助文檔中的一部分內(nèi)容
- 在函數(shù)語句塊的第一行,且習(xí)慣是多行的文本,所以多使用三引號
- 慣例是首字母大寫,第一行寫概述,空一行,第三行寫詳細(xì)描述
- 可以使用特殊屬性
__doc__訪問這個文檔字符串

3.1 自定義文檔字符串
def add(x,y):
"""This is a function of addition"""
a = x+y
return x + y
print("name = {}\ndoc = {}".format(add.__name__, add.__doc__))
print(help(add))
#以上代碼執(zhí)行結(jié)果如下:
name = add
doc = This is a function of addition
Help on function add in module __main__:
add(x, y)
This is a function of addition
None
3.2 裝飾器的副作用
def logger(fn):
def wrapper(*args,**kwargs):
'I am wrapper'
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper
@logger #add = logger(add)
def add(x,y):
'''This is a function for add'''
return x + y
print("name = {}\ndoc= {}".format(add.__name__, add.__doc__)) #使用裝飾器,原函數(shù)對象的屬性都被替換了,我們的需求是查看被封裝函數(shù)的屬性,如何解決?
3.3 解決裝飾器的副作用
提供一個函數(shù),copy 被裝飾函數(shù)屬性到裝飾器函數(shù)中去。
def copy_properties(src): # 柯里化
def _copy_properties(dst):
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
return dst
return _copy_properties
def logger(fn):
@copy_properties(fn) # wrapper = copy_properties(fn)(wrapper)
def wrapper(*args,**kwargs):
'I am wrapper'
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper
@logger #add = logger(add)
def add(x,y):
'''This is a function for add'''
return x + y
print("name = {}\ndoc = {}".format(add.__name__, add.__doc__))
#以上代碼執(zhí)行結(jié)果如下:
name = add
doc = This is a function for add
通過 copy_properties 函數(shù)將被包裝函數(shù)的屬性覆蓋掉包裝函數(shù)的屬性,凡是被裝飾的函數(shù)都需復(fù)制這些屬性,這個函數(shù)很通用。
而在Python中,為了解決此問題,提供了wraps修改被裝飾的doc信息。
from functools import wraps
def logger(fn):
@wraps(fn) #其實查看wraps源碼是利用update_wrapper()實現(xiàn)的(需要有偏函數(shù)知識),但是實際開發(fā)中我們推薦使用wraps裝飾去。
def wrapper(*args,**kwargs):
'I am wrapper'
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper
@logger #add = logger(add)
def add(x,y):
'''This is a function for add'''
return x + y
print("name = {}\ndoc = {}".format(add.__name__, add.__doc__))
#以上代碼執(zhí)行結(jié)果如下:
name = add
doc = This is a function for add
四、裝飾器分類
4.1 無參裝飾器
需求:設(shè)計裝飾器來獲取函數(shù)執(zhí)行時長。
import datetime, time
def logger(fn):
def wrap(*args, **kwargs):
# before 功能增強
print('args={}, kwargs={}'.format(args, kwargs))
start = datetime.datetime.now()
ret = fn(*args, **kwargs)
# after 功能增強
duration = datetime.datetime.now() - start
print("function {} took {}s".format(fn.__name__, duration.total_seconds()))
return ret
return wrap
@logger # 相當(dāng)于add = logger(add),調(diào)用裝飾器
def add(x, y):
print("===call add===========")
time.sleep(2)
return x + y
print(add(1,2))
#以上代碼輸出結(jié)果如下:
args=(1, 2), kwargs={}
===call add===========
function add took 2.000522s
3
4.2 帶參裝飾器
需求:設(shè)計裝飾器來獲取函數(shù)執(zhí)行時長,并對時長超過閾值的函數(shù)記錄一下。
import datetime, time
from functools import wraps
def logger(duration):
def _logger(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
ret = fn(*args, **kwargs)
delta = (datetime.datetime.now() - start).total_seconds()
print('it\'s so slow') if delta > duration else print('it\'s so fast')
print('function {} took {}s'.format(fn.__name__, delta))
return ret
return wrapper
return _logger
@logger(2)
def add(x, y):
print("===call add===========")
time.sleep(2)
return x + y
print(add(1,2))
#以上代碼執(zhí)行結(jié)果如下:
===call add===========
it's so slow
function add took 2.001955s
3
為了傳多一個參數(shù)進去裝飾器函數(shù)中,多加了一層嵌套。這樣就會先執(zhí)行 logger(2),返回_logger。這樣又回到 @_logger 無參裝飾器的情況,把被裝飾函數(shù)傳進 @_logger,函數(shù)功能將得到增強。
改進:將記錄的功能提取出來,這樣就可以通過外部提供的函數(shù)來靈活的控制輸出。
import datetime, time
from functools import wraps
# 通過 func 參數(shù),可自定義一個輸出函數(shù)來控制輸出格式
def logger(duration, func=lambda name, duration: print('function {} took {}s'.format(name, duration))):
def _logger(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
ret = fn(*args, **kwargs)
delta = (datetime.datetime.now() - start).total_seconds()
print('it\'s so slow') if delta > duration else print('it\'s so fast')
func(fn.__name__, delta) # func 參數(shù)是一個函數(shù)
return ret
return wrapper
return _logger
@logger(2)
def add(x, y):
print("===call add===========")
time.sleep(2)
return x + y
print(add(1,2))
#以上代碼執(zhí)行結(jié)果如下:
===call add===========
it's so slow
function add took 2.000923s
3
五、functools.update_wrapper
5.1 概述
語法:
functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS,updated=WRAPPER_UPDATES)
#
wrapper:包裝函數(shù)、被更新者
wrapped:被包裝函數(shù)、數(shù)據(jù)源
assigned:是元組,用于指定將原始函數(shù)的哪些屬性直接分配給包裝函數(shù)上的匹配屬性。
默認(rèn)是模塊級常量WRAPPER_ASSIGNMENTS,其中是要被覆蓋的屬性'__module__', '__name__', '__qualname__', '__doc__', '__annotations__',即模塊名、名稱、限定名、文檔、參數(shù)注解
updated:使用原函數(shù)的相應(yīng)屬性更新包裝函數(shù)的哪些屬性。
默認(rèn)是模塊級常量WRAPPER_UPDATES,其中是要被更新的屬性(更新wrapper的__dict__,即實例字典)
wrapper增加了一個__wrapped__屬性,保留著wrapped函數(shù)
功能:類似copy_properties功能,用于保護更新包裝函數(shù),使其看起來像被包裝函數(shù)(屬性和被包裝函數(shù)保持一致)
5.2 實例
import datetime, time, functools
def logger(duration, func=lambda name, duration: print('{} took {}s'.format(name, duration))):
def _logger(fn):
@functools.wraps(fn)
def wrapper(*args,**kwargs):
start = datetime.datetime.now()
ret = fn(*args,**kwargs)
delta = (datetime.datetime.now() - start).total_seconds()
if delta > duration:
func(fn.__name__, duration)
return ret
return wrapper
return _logger
@logger(5) # add = logger(5)(add)
def add(x,y):
time.sleep(1)
return x + y
print(add(5, 6), add.__name__, add.__wrapped__, add.__dict__, sep='\n')
#以上代碼執(zhí)行結(jié)果如下:
11 # 打印的是add(5, 6)結(jié)果
add # add.__name__ 被裝飾后的函數(shù)名
<function add at 0x0000000002A0F378> # add.__wrapped__ 保留著被裝飾函數(shù)
{'__wrapped__': <function add at 0x0000000002A0F378>} # add.__dict__ 屬性字典
5.3 裝飾器的調(diào)用過程
import datetime, time, functools
def logger(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
ret = fn(*args, **kwargs)
delta = (datetime.datetime.now() - start).total_seconds()
if delta > 3:
print('so slow')
return ret
return wrapper
@logger
def add(x,y):
pass
@logger
def sub(x,y):
pass
print(add.__name__, sub.__name__)
查看上面的代碼,思考:
- logger什么時候執(zhí)行
- 解釋器讀到 16、20行就被調(diào)用
- logger執(zhí)行幾次
- 2次,16、20行
- wraps裝飾器執(zhí)行幾次
- 2次,因為 wraps 裝飾器在 logger 才會被調(diào)用,logger執(zhí)行2次,wraps 裝飾器執(zhí)行兩次
- wrapper的
__name__被覆蓋過幾次- 各1次
-
print(add.__name__, sub.__name__)打印了什么- add sub
由上可知:
- 裝飾器函數(shù),在語法糖一被讀取時,就調(diào)用裝飾器函數(shù)了,而不是等到被裝飾函數(shù)被調(diào)用的時候
六、裝飾器的用途和應(yīng)用場景
用途:
裝飾器是AOP面向切面編程 Aspect Oriented Programming 的思想的體現(xiàn)。
面向?qū)ο笸枰ㄟ^繼承或者組合依賴等方式調(diào)用一些功能,這些功能的代碼往往可能再多個類中出現(xiàn),例如logger功能代碼。這樣造成代碼的重復(fù),增加了耦合。loggger的改變影響所有其它的類或方法。
而AOP再許喲啊的類或者方法上切下,前后的切入點可以加入增強的功能。讓調(diào)用者和被調(diào)用者解耦,這是一種不修改原來的業(yè)務(wù)代碼,給程序員動態(tài)添加功能的技術(shù)。例如logger函數(shù)就是對業(yè)務(wù)函數(shù)增加日志的功能,而業(yè)務(wù)函數(shù)中應(yīng)該把業(yè)務(wù)無關(guān)的日志功能剝離干凈。
使用場景:
日志,監(jiān)控,權(quán)限,審計,參數(shù)檢查,路由等處理。
這些功能與業(yè)務(wù)功能無關(guān),是很多都需要的公有的功能,所有適合獨立出來,需要的時候,對目標(biāo)對象進行增強。
簡單講:缺什么,補什么。