20.裝飾器
20.1 函數(shù)基礎(chǔ)知識(shí)
? ? 在Python中函數(shù)為一等公民适袜,我們可以:
- 把函數(shù)賦值給變量
- 在函數(shù)中定義函數(shù)
- 在函數(shù)中返回函數(shù)
- 把函數(shù)傳遞給函數(shù)
20.1.1 把函數(shù)賦值給變量
? ? 在Python里仿村,函數(shù)是對(duì)象遇绞,因此可以把它賦值給變量,如下所示:
def hello(name="Surpass"):
return "Hello,"+name.capitalize()
? ? 上述代碼定義了一個(gè)函數(shù)hello()康栈,具體功能是把對(duì)輸入的姓名打招呼属韧,并將姓名首字母轉(zhuǎn)換為大寫掩完。下面將函數(shù)賦值給變量,如下所示:
func=hello
print(func)
其輸出結(jié)果如下所示:
<function hello at 0x0000029D25213048>
? ? 在上述代碼中充活,將hello賦值給func并輸出打印結(jié)果蜂莉。從結(jié)果上來看蜡娶,當(dāng)不帶括號(hào)使用hello時(shí),僅輸出函數(shù)對(duì)象映穗,而非調(diào)用函數(shù)窖张,當(dāng)帶上括號(hào)時(shí),則表示調(diào)用函數(shù)蚁滋,如下所示:
func=hello
print(func())
其輸出結(jié)果如下所示:
Hello,Surpass
? ? 既然函數(shù)是對(duì)象荤堪,那是不是可以嘗試刪除對(duì)象,來看看以下代碼:
def hello(name="Surpass"):
return "Hello,"+name.capitalize()
func=hello
del hello
try:
print(hello())
except Exception as ex:
print(ex)
print(func())
其輸出結(jié)果如下所示:
name 'hello' is not defined
Hello,Surpass
? ? 在上面代碼中枢赔,雖然hello函數(shù)已經(jīng)刪除澄阳,但在刪除之前,已經(jīng)賦值給變量func踏拜,所以不影響func的功能碎赢。因此可以得出以下總結(jié):
函數(shù)是對(duì)象,可以將基賦值給其他變量variable速梗,在調(diào)用時(shí)肮塞,需要添加括號(hào)variable()
20.1.2 在函數(shù)中定義函數(shù)
? ? 在Python中,可以在函數(shù)中定義函數(shù)姻锁,示例代碼如下所示:
def hello(name="Surpass"):
def welcome(country="China"):
return "Hello,"+name.capitalize()+",Welcome to "+country
print(welcome())
hello()
其輸出結(jié)果如下所示:
Hello,Surpass,Welcome to China
? ? 上述示例中枕赵,內(nèi)部函數(shù)welcome表示歡迎來到某個(gè)國家,而且這個(gè)函數(shù)位于hello函數(shù)內(nèi)部位隶,只有通過調(diào)用hello函數(shù)時(shí)才能生效拷窜,測(cè)試代碼如下所示:
def hello(name="Surpass"):
def welcome(country="China"):
return "Hello,"+name.capitalize()+",Welcome to "+country
print(welcome())
try:
print(welcome())
except Exception as ex:
print(ex)
其輸出結(jié)果如下所示:
name 'welcome' is not defined
? ? 通過上面的示例代碼可以得到以下總結(jié):
在函數(shù)funcA中可以定義函數(shù)funcB,但funcB無法在函數(shù)funcA之外進(jìn)行調(diào)用
20.1.3 在函數(shù)中返回函數(shù)
? ? 通過前面兩個(gè)示例涧黄,函數(shù)即可以返回其值篮昧,也可以返回其函數(shù)對(duì)象,示例代碼如下所示:
def hello(type=1):
def name(name="Surpass"):
return "My name is "+name
def welcome(country="China"):
return " Welcome to "+country
def occupation(occupation="engineer"):
return " I am "+occupation
if type==1:
return name
elif type==2:
return welcome
else:
return occupation
? ? 在函數(shù)hello中定義了3個(gè)函數(shù)笋妥,然后根據(jù)type參數(shù)來返回不同的信息懊昨,在hello函數(shù)中,返回都為函數(shù)對(duì)象春宣,可視為變量酵颁,運(yùn)行以下代碼:
print(hello(1))
print(hello(1)())
func=hello(1)
print(func())
其輸出結(jié)果如下所示:
<function hello.<locals>.name at 0x000001D65783D5E8>
My name is Surpass
My name is Surpass
? ? 注意上面在寫代碼上的區(qū)別,如果不加括號(hào)月帝,則代表僅返回函數(shù)對(duì)象躏惋,添加括號(hào)則代表調(diào)用函數(shù),總結(jié)如下所示:
在函數(shù)funcA中可以定義多個(gè)函數(shù)funcB...嫁赏,并且可以把funcB當(dāng)成對(duì)象進(jìn)行返回
20.1.4 把函數(shù)傳遞給函數(shù)
? ? 在Python中其掂,既然一切都為對(duì)象,那函數(shù)也可以做參數(shù)傳遞給另一個(gè)函數(shù)潦蝇,示例代碼如下所示:
import datetime
import time
def hello(name="Surpass"):
print(f"Hello,{name}")
def testFunc(func):
print(f"current time is {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
hello()
time.sleep(2)
print(f"current time is {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
testFunc(hello)
其輸出結(jié)果如下所示:
current time is 2020-05-17 14:58:56
Hello,Surpass
current time is 2020-05-17 14:58:58
? ? 在函數(shù)testFunc中接收參數(shù)為函數(shù)類型的函數(shù)hello款熬,而且并未改變函數(shù)hello的任何內(nèi)容深寥,且實(shí)現(xiàn)在函數(shù)hello前后增加一些內(nèi)容。因此我們可以總結(jié)如下所示:
函數(shù)funcA傳遞到函數(shù)funcB贤牛,函數(shù)funcB僅在函數(shù)funcA運(yùn)行前后有操作惋鹅,但不改變funcA,這就是裝飾器的錐形殉簸。
20.2 裝飾器
20.2.1 閉包
? ? 維基的解釋如下所示:
在計(jì)算機(jī)科學(xué)中闰集,閉包(Closure),又稱詞法閉包(Lexical Closure)或函數(shù)閉包(function closures)般卑,是引用了自由變量的函數(shù)武鲁。這個(gè)被引用的自由變量將和這個(gè)函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外蝠检。所以沐鼠,有另一種說法認(rèn)為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實(shí)體。閉包在運(yùn)行時(shí)可以有多個(gè)實(shí)例叹谁,不同的引用環(huán)境和相同的函數(shù)組合可以產(chǎn)生不同的實(shí)例饲梭。
? ? Python里面的閉包是一種高階函數(shù),返回值為函數(shù)對(duì)象焰檩,簡(jiǎn)單來講就是一個(gè)函數(shù)定義中引用了函數(shù)外定義的變量憔涉,并且該函數(shù)可以在其定義環(huán)境外執(zhí)行,這種函數(shù)稱之為閉包析苫。詳細(xì)如下圖所示:
? ? 在函數(shù)make_averager中兜叨,series為make_averager函數(shù)的局部變量,在averager函數(shù)中series稱之為自由變量(未在本地作用域中綁定的變量)
示例如下所示:
def outter(name):
def inner():
print(f"para is {name}")
return inner
? ? 內(nèi)部函數(shù)inner()可以使用外部函數(shù)outter()的參數(shù)name藤违,最后返回內(nèi)部函數(shù)對(duì)象inner浪腐∽葑幔可以按照在函數(shù)中返回函數(shù)進(jìn)行調(diào)用《倨梗現(xiàn)在我們將outter換成decorator,inner換成wrapper泽谨,如下所示:
def decorator(name):
def wrapper():
print(f"para is {name}")
return wrapper
? ? 以上這種形式就是裝飾器璧榄,返回值為wrapper對(duì)象并等候調(diào)用,一旦被調(diào)用就運(yùn)行 print(f"para is {name}")輸出結(jié)果吧雹。對(duì)于裝飾器而言骨杂,嚴(yán)格定義來講,參數(shù)是函數(shù)而不是變量雄卷,常用形式如下所示:
def decorator(func):
def wrapper():
return func()
return wrapper
20.2.2 裝飾器初體驗(yàn)
? ? 我們先來看看一個(gè)簡(jiǎn)單的示例搓蚪,代碼如下所示:
def hello():
print("call hello function")
def decorator(func):
def wrapper():
return func()
return wrapper
tmp=decorator(hello)
tmp()
其輸出結(jié)果如下所示:
call hello function
? ? 裝飾器的特性就是給原函數(shù)做裝飾,但不改變?cè)瘮?shù)的內(nèi)容丁鹉,在以下代碼中妒潭,希望在運(yùn)行原函數(shù)func()之前悴能,輸出原函數(shù)的名字:
def hello():
print("call hello function")
def decorator(func):
def wrapper():
print(f"before call func name is {func.__name__}")
return func()
return wrapper
tmp=decorator(hello)
tmp()
其輸出結(jié)果如下所示:
before call func name is hello
call hello function
? ? 經(jīng)過前面的不斷學(xué)習(xí),相信已經(jīng)初步掌握了裝飾器的原理雳灾,如果每次都這樣調(diào)用是不是太麻煩了漠酿,于是Python提供了一種語法糖@裝飾函數(shù)寫在被裝飾函數(shù)上面即可。如下所示:
def decorator(func):
def wrapper():
print(f"before call func name is {func.__name__}")
return func()
return wrapper
@decorator
def hello():
print("call hello function")
hello()
其輸出結(jié)果如下所示:
before call func name is hello
call hello function
? ? 上面這種寫法等價(jià)于
tmp=decorator(hello)
tmp()
語法糖 (syntactic sugar):指計(jì)算機(jī)語言中添加的某種語法谎亩,對(duì)語言的功能沒有影響炒嘲,但是讓程序員更方便地使用。
20.2.3 裝飾器知識(shí)點(diǎn)
20.2.3.1 多個(gè)裝飾器
? ? 即裝飾器的特性是給函數(shù)進(jìn)行裝飾匈庭,那是不是一個(gè)函數(shù)可以被多個(gè)函數(shù)進(jìn)行裝飾夫凸,定義一個(gè)函數(shù)如下所示:
def hello(name="Surpass"):
return f"Hello,{name}"
? ? 針對(duì)以上這個(gè)函數(shù),我們希望能完成以下功能:
- 字符串全部大寫
- 將返回的字符串拆分成單個(gè)單詞列表
? ? 為完成以上功能阱持,我們定義兩個(gè)裝飾器如下所示:
# 裝飾器一:
def toUpper(func):
def wrapper():
return func().upper()
return wrapper
# 裝飾器二:
def splitStr(func):
def wrapper():
return func().split(",")
return wrapper
@splitStr
@toUpper
def hello(name="Surpass"):
return f"Hello,{name}"
print(hello())
其輸出結(jié)果如下所示:
['HELLO', 'SURPASS']
? ? 以下代碼等價(jià)于:
print(splitStr(toUpper(hello))())
總結(jié)一下:一個(gè)函數(shù)存在多個(gè)裝飾器寸痢,順序是按照就近原則,即哪個(gè)函數(shù)靠近被裝飾函數(shù)紊选,則優(yōu)先進(jìn)行裝飾啼止,上面示例中@toUpper離被裝飾函數(shù)hello最近,優(yōu)先運(yùn)行兵罢,依次類推献烦。
20.2.3.2 傳遞參數(shù)給裝飾函數(shù)
? ? 裝飾函數(shù)就是wrapper(),因?yàn)橐{(diào)用原函數(shù)func卖词,一旦它有參數(shù)巩那,則需要將這些參數(shù)傳遞給wrapper(),示例如下所示:
1.沒有參數(shù)的wrapper()
def decorator(func):
def wrapper():
funcName=func.__name__
print(f"Befor call {funcName}")
func()
print(f"After call {funcName}")
return wrapper
@decorator
def testFunc():
print(f"call function is testFunc")
testFunc()
其輸出結(jié)果如下所示:
Befor call testFunc
call function is testFunc
After call testFunc
2.傳固定參數(shù)個(gè)數(shù)的wrapper()
? ? 如果被裝飾函數(shù)有傳入?yún)?shù)此蜈,則在裝飾函數(shù)添加參數(shù)即可即横。
def decorator(func):
def wrapper(args1,args2):
funcName=func.__name__
print(f"Befor call {funcName}")
func(args1,args2)
print(f"After call {funcName}")
return wrapper
@decorator
def testFunc(name,age):
print(f"Name is {name},age is {str(age)}")
testFunc("Surpass",28)
其輸出結(jié)果如下所示:
Befor call testFunc
Name is Surpass,age is 28
After call testFunc
3.帶返回值的wrapper()
? ? 如果被裝飾函數(shù)有返回,則在裝飾函數(shù)中將被裝飾函數(shù)的結(jié)果賦給變量返回即可裆赵。
def decorator(func):
def wrapper(args1,args2):
funcName=func.__name__
print(f"Befor call {funcName}")
result=func(args1,args2)
print(f"After call {funcName}")
return result
return wrapper
@decorator
def testFunc(name,age):
return f"Name is {name},age is {str(age)}"
print(testFunc("Surpass",28))
其輸出結(jié)果如下所示:
Befor call testFunc
After call testFunc
Name is Surpass,age is 28
4.無固定參數(shù)個(gè)數(shù)的wrapper()
? ? 在學(xué)習(xí)Python函數(shù)時(shí)东囚,如果參數(shù)較多或無法確定參數(shù)個(gè)數(shù),可以使用位置參數(shù)(*agrs)也可以使用關(guān)鍵字參數(shù)(**kwargs)战授,示例代碼如下所示:
def decorator(func):
def wrapper(*args,**kwargs):
funcName=func.__name__
print(f"Befor call {funcName}")
result=func(*args,**kwargs)
print(f"After call {funcName}")
return result
return wrapper
@decorator
def testFunc(*args,**kwargs):
sum,dicSum=0,0
for item in args:
sum+=item
for k,v in kwargs.items():
dicSum+=v
return sum,dicSum
print(testFunc(1,2,3,4,5,key1=1,key2=2,key3=5,key4=1000))
其輸出結(jié)果如下所示:
Befor call testFunc
After call testFunc
(15, 1008)
20.2.3.3 functools.wrap
? ? 在裝飾器中页藻,裝飾之后的函數(shù)名稱會(huì)變亂,如一個(gè)函數(shù)為hello()植兰,其名稱為hello
def hello():
pass
hello.__name__
其最終的函數(shù)名為:
'hello'
? ? 而在裝飾器中份帐,在使用@裝飾函數(shù)名稱之后,其名稱已經(jīng)變成wrapper楣导,原因也非常簡(jiǎn)單废境,因?yàn)槲覀冏罱K的調(diào)用形式都是decorator(被裝飾函數(shù)),而這個(gè)函數(shù)返回的是wrapper()函數(shù),因此名稱為wrapper噩凹。示例代碼如下所示:
def decorator(func):
def wrapper():
return func()
return wrapper
@decorator
def testFunc():
pass
print(testFunc.__name__)
其輸出結(jié)果如下所示:
wrapper
? ? 這種情況下朦促,多種情況下應(yīng)該不需要關(guān)注,但如果需要根據(jù)函數(shù)名來進(jìn)行一些操作(如反射)則會(huì)出錯(cuò)栓始。如果仍然希望保持函數(shù)的原名务冕,可以使用functools.wraps,示例代碼如下所示:
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper():
return func()
return wrapper
@decorator
def testFunc():
pass
print(testFunc.__name__)
其輸出結(jié)果如下所示:
testFunc
仔細(xì)看上面代碼幻赚,是不是裝飾函數(shù)又被裝飾了一次禀忆?
20.2.3.4 傳遞參數(shù)給裝飾器
? ? 除了可以傳遞參數(shù)給裝飾函數(shù)(wapper),也可以傳遞參數(shù)給裝飾函數(shù)(decorator)落恼。來看看以下示例箩退,在示例中,只有裝飾函數(shù)有參數(shù)佳谦,裝飾器將數(shù)值保留2位小數(shù)戴涝,如下所示:
def digitalFormat(func):
def wrapper(*args,**kwargs):
result=func(*args,**kwargs)
formatResult=round(result,2)
return formatResult
return wrapper
@digitalFormat
def add(a:float,b:float)->float:
return a+b
print(add(12.09,19.12345))
其輸出結(jié)果如下所示:
31.21
? ? 通過裝飾器,我們很快就達(dá)到要求钻蔑。但有些情況啥刻,單純的數(shù)值可能并沒有太大意義,需要結(jié)合單位咪笑。假設(shè)上面示例返回為重量可帽,則單位可能為g、kg等窗怒。那這種需求有沒有解決辦法了映跟?示例代碼如下所示:
def unit(unit:str)->str:
def digitalFormat(func):
def wrapper(*args,**kwargs):
result=func(*args,**kwargs)
formatResult=f"{round(result,2)} {unit}"
return formatResult
return wrapper
return digitalFormat
@unit("kg")
def add(a:float,b:float)->float:
return a+b
print(add(12.09,19.12345))
其輸出結(jié)果如下所示:
31.21 kg
如果裝飾飾器本身需要傳遞參數(shù),那么再定義一層函數(shù)扬虚,將裝飾的參數(shù)傳入即可努隙,是不是很簡(jiǎn)單。
20.2.3.5 類裝飾器
1.使用類去裝飾函數(shù)
? ? 前面實(shí)現(xiàn)的裝飾器都是針對(duì)函數(shù)而言辜昵,在實(shí)際應(yīng)用中荸镊,類也可以作為裝飾器。在類裝飾器中主要依賴函數(shù)call()路鹰,每調(diào)用一個(gè)類的示例時(shí)贷洲,函數(shù)call()就會(huì)被執(zhí)行一次,示例代碼如下所示:
class Count:
def __init__(self,func):
self.func=func
self.callCount=0
def __call__(self, *args, **kwargs):
self.callCount+=1
print(f"call count is {self.callCount}")
return self.func(*args,**kwargs)
@Count
def testSample():
print("Hello, Surpass")
for i in range(3):
print(f"first call {testSample()}")
其輸出結(jié)果如下所示:
call count is 1
Hello, Surpass
first call None
call count is 2
Hello, Surpass
first call None
call count is 3
Hello, Surpass
first call None
2.使用函數(shù)去裝飾類
def decorator(num):
def wrapper(cls):
cls.callCount=num
return cls
return wrapper
@decorator(10)
class Count:
callCount = 0
def __init__(self):
pass
def __call__(self, *args, **kwargs):
self.callCount+=1
print(f"call count is {self.callCount}")
return self.func(*args,**kwargs)
if __name__ == '__main__':
count=Count()
print(count.callCount)
其輸出結(jié)果如下所示:
10
20.4.4 何時(shí)使用裝飾器
? ? 這個(gè)需要據(jù)實(shí)際的需求而定晋柱。比如需要測(cè)試每個(gè)函數(shù)運(yùn)行時(shí)間,此時(shí)可以使用裝飾器诵叁,很輕松就能達(dá)到雁竞。另外裝飾器也可應(yīng)用到身價(jià)認(rèn)證、日志記錄和輸入合理性檢查等等。
20.4.5 裝飾器實(shí)際案例
? ? 在實(shí)際工作中碑诉,裝飾器經(jīng)常會(huì)用來記錄日志和時(shí)間彪腔,示例如下所示:
from functools import wraps
import logging
import time
def logType(logType):
def myLogger(func):
logging.basicConfig(filename=f"{func.__name__}.log",level=logging.INFO)
@wraps(func)
def wrapper(*args,**kwargs):
logging.info(f" {logType} Run with args:{args} and kwargs is {kwargs}")
return func(*args,**kwargs)
return wrapper
return myLogger
def myTimer(func):
@wraps(func)
def wrapper(*args,**kwargs):
startTime=time.time()
result=func(*args,**kwargs)
endTime=time.time()
print(f"{func.__name__} run time is {endTime-startTime} s ")
return result
return wrapper
@logType("Release - ")
@myTimer
def testWrapperA(name:str,color:str)->None:
time.sleep(5)
print(f"{testWrapperA.__name__} input paras is {(name,color)}")
@logType("Test - ")
@myTimer
def testWrapperB(name:str,color:str)->None:
time.sleep(5)
print(f"{testWrapperB.__name__} input paras is {(name,color)}")
if __name__ == '__main__':
testWrapperA("apple","red")
testWrapperB("apple", "red")
其輸出結(jié)果如下所示:
testWrapperA input paras is ('apple', 'red')
testWrapperA run time is 5.000441789627075 s
testWrapperB input paras is ('apple', 'red')
testWrapperB run time is 5.000349521636963 s
日志記錄信息如下所示:
20.4.6 小結(jié)
? ? 裝飾器就是接受參數(shù)類型為函數(shù)或類并返回函數(shù)或類的函數(shù),通過裝飾器函數(shù)进栽,在不改變?cè)瘮?shù)的基礎(chǔ)上添加新的功能德挣。示意圖如下所示: