一拓颓、裝飾器的基本使用
在不改變函數(shù)源代碼的前提下芙扎,給函數(shù)添加新的功能,這時(shí)就需要用到“裝飾器”牍白。
0.開(kāi)放封閉原則
寫(xiě)代碼要遵循開(kāi)放封閉原則,它規(guī)定“已經(jīng)實(shí)現(xiàn)的功能代碼不允許被修改抖棘,但可以被擴(kuò)展茂腥。”——封閉即是已經(jīng)實(shí)現(xiàn)的功能代碼塊切省,開(kāi)放是對(duì)擴(kuò)展開(kāi)放最岗。
1.裝飾器本質(zhì)
裝飾器本質(zhì)上就是一個(gè)閉包 ,但是是一個(gè)特殊的閉包朝捆。當(dāng)閉包函數(shù)有且只有一個(gè)參數(shù)般渡,并且該參數(shù)必須是函數(shù)類型,此時(shí)閉包才稱為“裝飾器”芙盘。
2.裝飾器的功能特點(diǎn)
1)給已有函數(shù)增加額外的功能驯用;
2)但是不修改已有函數(shù)的源代碼;
3)不修改已有函數(shù)的調(diào)用方式儒老;
3.裝飾器定義格式
# 定義裝飾器
def decorator(func):
? ? def wrapper(*args, **kwargs):
? ? ? ? 要添加的額外功能
? ? ? ? return func(*args, **kwargs)
? ? return wrapper
其中:
1)func:是一個(gè)函數(shù)(函數(shù)作為參數(shù))蝴乔;
2)wrapper:內(nèi)層閉包函數(shù),調(diào)用了func驮樊;
3)return wrapper:外層函數(shù)decorator返回值為內(nèi)層函數(shù)wrapper薇正;
4.裝飾器的使用
先定義要裝飾的函數(shù):
def print_info():
? ? print('打印的內(nèi)容……')
為函數(shù)print_info添加功能:打印前提示,打印后提示
# 定義裝飾器
def decorator(func):
? ? def wrapper(*args, **kwargs):
? ? ? ? print('***我要開(kāi)始打印正文了***')
? ? ? ? func()
? ? ? ? print('***全文已打印完畢***')
? ? ? ? return func(*args, **kwargs)
? ? return wrapper
# 使用裝飾器:將print_info函數(shù)作為裝飾器的參數(shù)
print_info = decorator(print_info)
print_info()
>>> 運(yùn)行結(jié)果
***我要開(kāi)始打印正文了***
打印的內(nèi)容……
***全文已打印完畢***
可以看出巩剖,1)為函數(shù)print_info添加功能成功铝穷;2)沒(méi)有修改原函數(shù)钠怯;3)原函數(shù)調(diào)用方式也沒(méi)變佳魔;
裝飾器內(nèi)部運(yùn)行原理:
說(shuō)明:
1)print_info = decorator(print_info),此處只裝飾函數(shù)晦炊,并且把裝飾器的內(nèi)層函數(shù)返回鞠鲜,并賦值給print_info,此時(shí)print_info實(shí)際已經(jīng)是wrapper函數(shù)断国;
2)print_info()贤姆,此處調(diào)用函數(shù)時(shí),才正式開(kāi)始執(zhí)行已經(jīng)添加功能的函數(shù)(wrapper函數(shù))稳衬;
3)wrapper函數(shù)中的func()調(diào)用的是原函數(shù)霞捡,因?yàn)殚]包函數(shù)能夠保存它使用的外層函數(shù)的變量;
5.注意事項(xiàng)
由上面可以看出裝飾原函數(shù)后薄疚,原函數(shù)print_info的函數(shù)名已經(jīng)變成wrapper(print_info.__name__='wrapper')碧信。
為了不改變?cè)瘮?shù)的函數(shù)名赊琳,需要把原始函數(shù)的__name__等屬性復(fù)制到wrapper()函數(shù)中,否則砰碴,有些依賴函數(shù)簽名的代碼執(zhí)行就會(huì)出錯(cuò)躏筏。Python內(nèi)置的裝飾器functools.wraps能夠?qū)崿F(xiàn)。因此嚴(yán)格意義上裝飾器的定義應(yīng)為:
import functools
def decorator(func):
? ? @functools.wraps(func)
? ? def wrapper(*args, **kwargs):
? ? ? ? 要添加的額外功能
? ? ? ? return func(*args, **kwargs)
? ? return wrapper
6.裝飾器的裝飾方式
方式一:直接調(diào)用
func = decorator(func)? # 直接調(diào)用裝飾器函數(shù)呈枉,傳入函數(shù)名趁尼,并賦值給函數(shù)名
但每次都寫(xiě)調(diào)用-賦值語(yǔ)句比較麻煩,于是Python提供了比較簡(jiǎn)單的第二種“語(yǔ)法糖”書(shū)寫(xiě)格式
方式二:語(yǔ)法糖--@裝飾器名字
@decorator? ? ? ? ? ? ? # 在函數(shù)定義語(yǔ)句上面直接“@裝飾器名字”
def func():
? ? ...
語(yǔ)法糖裝飾是常用的裝飾器裝飾方式猖辫,“@decorator”就等價(jià)于func = decorator(func)酥泞。
裝飾器在模塊進(jìn)行加載時(shí)就會(huì)立即執(zhí)行裝飾過(guò)程。
7.裝飾器使用場(chǎng)景
1)引入日志啃憎;
2)函數(shù)執(zhí)行時(shí)間統(tǒng)計(jì)婶博;
3)執(zhí)行函數(shù)前預(yù)備處理;
4)執(zhí)行函數(shù)后清理功能荧飞;
5)權(quán)限校驗(yàn)等場(chǎng)景凡人;
6)緩存;
8.應(yīng)用舉例
統(tǒng)計(jì)函數(shù)的執(zhí)行時(shí)間:
# 統(tǒng)計(jì)函數(shù)執(zhí)行時(shí)間的裝飾器
import functools
import time
def execute_time(func):
? ? @functools.wraps(func)
? ? def wrapper(*args, **kwargs):
? ? ? ? # 程序開(kāi)始時(shí)間點(diǎn)
? ? ? ? start = time.time()
? ? ? ? # 執(zhí)行函數(shù)
? ? ? ? result = func(*args, **kwargs)
? ? ? ? # 程序結(jié)束時(shí)間點(diǎn)
? ? ? ? end = time.time()
? ? ? ? # 計(jì)算執(zhí)行時(shí)間
? ? ? ? execution_time = end - start
? ? ? ? print('{}執(zhí)行時(shí)間:{}s'.format(func.__name__, execution_time))
? ? ? ? # 返回函數(shù)結(jié)果
? ? ? ? return result
? ? return wrapper
# 定義一個(gè)簡(jiǎn)單的能夠指定循環(huán)次數(shù)的函數(shù)叹阔,并裝飾
@execute_time
def loop_count(n):
? ? # 循環(huán)打印
? ? for i in range(n):
? ? ? ? print('第{}次...'.format(i+1))?
# 看看循環(huán)1000000次需要多長(zhǎng)時(shí)間挠轴?
loop_count(1000000)
>>> 運(yùn)行結(jié)果:
第1次...
第2次...
...
第1000000次...
loop_count執(zhí)行時(shí)間:6.355999231338501s
二、通用裝飾器
要裝飾的函數(shù)可能不需要傳參耳幢,也可能需要傳參岸晦;然后傳多少個(gè)參數(shù),是位置傳參還是關(guān)鍵字傳參睛藻,有沒(méi)有返回值启上,這些都不能事先知道。也就是說(shuō)店印,不可能針對(duì)性地去定義裝飾器冈在。于是,就需要定義通用的裝飾器按摘。不論被裝飾的函數(shù)是否需要傳參包券,傳幾個(gè)參數(shù),怎么傳參炫贤,有無(wú)返回值溅固,通用裝飾器都能使用。
通用裝飾器的定義格式:
def decorator(func):
? ? def wrapper(*args, **kwargs):
? ? ? ? func執(zhí)行前要添加的額外功能
? ? ? ? result = func(*args, **args)
? ? ? ? func執(zhí)行后要添加的額外功能
? ? ? ? return result
? ? return wrapper
說(shuō)明:
1)內(nèi)層閉包函數(shù)使用*args兰珍、**kwargs接收參數(shù)侍郭,能夠應(yīng)對(duì)所有形式的不定長(zhǎng)傳參;
2)result為被裝飾函數(shù)的返回值,在閉包函數(shù)中return返回亮元;
解決了傳參和返回值的問(wèn)題汰寓,實(shí)現(xiàn)了通用性。
三苹粟、多個(gè)裝飾器
多個(gè)裝飾器同時(shí)裝飾一個(gè)函數(shù)
# 裝飾器1:將原函數(shù)輸入內(nèi)容用“()”括起
def add_little_sign(func):
? ? def wrapper1(*args, **kwargs):
? ? ? ? origin_info = func(*args, **kwargs)
? ? ? ? return '(' + origin_info + ')'
? ? return wrapper
# 裝飾器2:將原函數(shù)輸入內(nèi)容用“{}”括起
def add_big_sign(func):
? ? def wrapper2(*args, **kwargs):
? ? ? ? origin_info = func(*args, **kwargs)
? ? ? ? return '{' + origin_info + '}'
? ? return wrapper
# 被裝飾函數(shù):打印info
@add_big_sign? ? ? # 外層
@add_little_sign? ? ? # 內(nèi)層
def print_info(info):
? ? return info
# 調(diào)用裝飾后的函數(shù)
print(print_info('Hello World!'))
>>> 運(yùn)行結(jié)果:
{(Hello World!)}
可以看到有滑,先實(shí)現(xiàn)了add_little_sign的功能,然后實(shí)現(xiàn)add_big_sign的功能嵌削。
1)上面語(yǔ)法糖的裝飾過(guò)程等價(jià)于:
print_info = add_little_sign(print_info)
print_info = add_big_sign(print_info)
裝飾過(guò)程:
原print_info函數(shù) --> 執(zhí)行“print_info = add_little_sign(print_info)” 毛好,使用add_little_sign裝飾原函數(shù)print_ifo --> print_info = wrapper1 --> 執(zhí)行“print_info = add_big_sign(print_info)” ,使用add_big_sign裝飾函數(shù)print_ifo(此時(shí)函數(shù)print_info已是函數(shù)wrapper1) --> print_info = wrapper2
當(dāng)裝飾完畢后苛秕,原print_info已經(jīng)變身成了wrapper2肌访,并且此時(shí)wrapper2中儲(chǔ)存著傳入的wrapper1。那么再調(diào)用此時(shí)的print_info艇劫,執(zhí)行過(guò)程為:調(diào)用裝飾后的print_info --> 調(diào)用wrapper2 --> wrapper2內(nèi)部調(diào)用wrapper1 --> 執(zhí)行wrapper1 --> 裝飾“()” --> 再裝飾“{}” --> 完畢吼驶。
2)使用多個(gè)裝飾器時(shí)功能特性
(1)一般使用語(yǔ)法糖方式裝飾;
(2)會(huì)先用內(nèi)層的裝飾店煞,再用外層的裝飾蟹演;
多個(gè)裝飾器同時(shí)使用時(shí),相當(dāng)于先將內(nèi)層的裝飾器包裹住原函數(shù)顷蟀,再在外面用外層的裝飾器包裹酒请。
四、帶參數(shù)的裝飾器
有時(shí)裝飾器也需要攜帶參數(shù)鸣个,以實(shí)現(xiàn)對(duì)要添加功能的處理羞反。
錯(cuò)誤的裝飾器攜帶參數(shù)的格式:
def decorator(func, a):
? ? def wrapper(*args, **kwargs):
? ? ? ? 要添加的額外功能
? ? ? ? (對(duì)參數(shù)a進(jìn)行操作)
? ? ? ? return func(*args, **kwargs)
? ? return wrapper
@decorator(2)? ? ? # 假如將參數(shù)a設(shè)置為2
def func():
? ? 代碼塊...
>>> 運(yùn)行結(jié)果:
TypeError: 'int' object is not callable? ?
因?yàn)檠b飾器只能接收一個(gè)參數(shù),并且該參數(shù)必須是可調(diào)用的函數(shù)對(duì)象囤萤。
正確的裝飾器攜帶參數(shù)的格式:
def create_decorator(a):
? ? def decorator(func):
? ? ? ? def wrapper(*args, **kwargs):
? ? ? ? ? ? 要添加的額外功能
? ? ? ? ? ? (對(duì)參數(shù)a進(jìn)行操作)
? ? ? ? ? ? return func(*args, **kwargs)
? ? ? ? return wrapper
? ? return decorator
@create_decorator(2)? ? ? # 假如將參數(shù)a設(shè)置為2
def func():
? ? 代碼塊...
>>> 運(yùn)行結(jié)果:
會(huì)正常運(yùn)行昼窗,不會(huì)再拋出異常? ?
上述代碼塊,定義了一個(gè)返回裝飾器的閉包涛舍,通過(guò)閉包能夠存儲(chǔ)外層函數(shù)變量的特性保存住外層函數(shù)的參數(shù)澄惊,然后在裝飾器中進(jìn)行處理。
裝飾過(guò)程等價(jià)于:
decorator = create_decorator(2)? ? # 傳入 參數(shù)2做盅,被儲(chǔ)存至返回的裝飾器內(nèi)
func = decorator(func)? ? ? # 這步開(kāi)始才是真正的裝飾器裝飾過(guò)程
...
五缤削、類裝飾器
裝飾器還有一種特殊的定義方法,就是通過(guò)定義一個(gè)類來(lái)定義裝飾器吹榴。
class Decorator(object):
? ? def __init__(self, func):
? ? ? ? # 初始化操作在此完成
? ? ? ? self.__func = func
? ? # 實(shí)現(xiàn)__call__方法,表示對(duì)象是一個(gè)可調(diào)用對(duì)象滚婉,可以像調(diào)用函數(shù)一樣進(jìn)行調(diào)用图筹。
? ? # 重寫(xiě)類的可調(diào)用魔法方法
? ? def __call__(self, *args, **kwargs):
? ? ? ? # 添加裝飾功能
? ? ? ? 要添加的額外功能
? ? ? ? self.__func()
@Decorator
def comment():
? ? print("發(fā)表評(píng)論")
comment()
說(shuō)明:
在定義類的初始化方法時(shí),定義一個(gè)私有實(shí)例屬性,此屬性是用來(lái)接收要裝飾的函數(shù)對(duì)象远剩;然后重寫(xiě)類的__call__方法扣溺,使對(duì)象被調(diào)用時(shí)實(shí)現(xiàn)原函數(shù)功能的基礎(chǔ)上增加新功能。(擁有__call__方法能夠使對(duì)象被調(diào)用瓜晤,“對(duì)象()”會(huì)自動(dòng)執(zhí)行對(duì)象的__call__方法锥余。)
上述裝飾器裝飾過(guò)程等價(jià)于:
comment = Decorator(comment)? ? # 傳入comment進(jìn)行實(shí)例化對(duì)象
comment()? ? ? # 此步將執(zhí)行類的實(shí)例方法__call__,從而實(shí)現(xiàn)了增加的功能
傳入原函數(shù)comment痢掠,Decorator接收并實(shí)例化出一個(gè)對(duì)象 --> comment = 實(shí)例化的對(duì)象 --> comment() --> 執(zhí)行實(shí)例對(duì)象的__call__方法 --> 完成裝飾驱犹。