** 裝飾器**
1.裝飾器的概念
- 裝飾器的本質(zhì)就是一個函數(shù),它的作用是為其他函數(shù)添加一個新的功能,但是不改變原函數(shù)的源代碼和調(diào)用方式。
-
裝飾器的兩大原則:
- 不修改被修飾函數(shù)的源代碼
- 不修改被修飾函數(shù)的調(diào)用方式
2.裝飾器的知識儲備(或者我們可以理解成,一個裝飾器是由什么組成)
- 裝飾器 = 高階函數(shù)+函數(shù)嵌套+閉包
3.裝飾器的實現(xiàn)
- 首先我們定義一個累加求和的函數(shù)
import time
def cal():
res = 0
for i in range(100):
res+=i
time.sleep(1)
print("函數(shù)的運行結(jié)果為",res)
return res
cal()
- 這是我們的一個初始函數(shù),如果此時我們需要為這個函數(shù)添加一個功能,例如我們需要計算這個函數(shù)需要的時間。想想好像也容易做到這一點的吧。通過調(diào)用函數(shù)前和函數(shù)運行結(jié)束后分別記下時間戳,那么我們就成功的為這個函數(shù)記下了運行的時間。
import time
def cal():
start_time = time.time()
res = 0
for i in range(100):
res += i
time.sleep(1)
stop_time = time.time()
print("函數(shù)的運行結(jié)果為", res)
print("函數(shù)的運行時間為",stop_time-start_time)
return res
cal()
- 通過上面的這種方法,我們很容易就達到了我們需要做得目的了,也就是說我們成功的為函數(shù)添加了一個新的功能——記錄函數(shù)運行的時間。不過,這只是其中的一個函數(shù),如果說有成百上千個函數(shù),我們也要這樣一個個的添加功能嗎?這顯然不太現(xiàn)實。
- 更關(guān)鍵的一點,我們這種做法已經(jīng)違反了開放封閉原則。開放封閉原則簡單的理解就是我們寫的程序上線以后,我們就不能對程序的源代碼經(jīng)行修改。如果函數(shù)在其他位置被調(diào)用了的話,我們便不知道會不會出現(xiàn)其他的后果,有可能引起一系列的連鎖反應(yīng)。所以說,上面的方法,我們是行不通的。
- 裝飾器的第二個原則: 不修改被修飾函數(shù)的調(diào)用方式 ,我可以為函數(shù)添加新的功能,但是不能改變它的調(diào)用方式,在上面的函數(shù)中,我們通過cal()進行調(diào)用函數(shù),那么我們添加了功能之后,我們就必須按照原來的方法,用cal()來調(diào)用函數(shù)。
-
高階函數(shù)的使用
-
首先我們說一下,什么是高階函數(shù)?
滿足下面兩個條件中的其中一個都可以成為高階函數(shù):- 函數(shù)接受的參數(shù)是一個函數(shù)名
- 函數(shù)的返回值是一個變量名
- 我們繼續(xù)拿上面的函數(shù)做例子
-
首先我們說一下,什么是高階函數(shù)?
import time
def cal():
res = 0
for i in range(100):
res+=i
time.sleep(1)
print("函數(shù)的運行結(jié)果為",res)
return res
def test(func):
print(func)
func()
test(cal)
那么這里的test就是一個高階函數(shù)。因為它接收的參數(shù)是一個函數(shù)名func,看到這里,我們就可以進行對函數(shù)cal添加新的功能了,繼續(xù)以記錄函數(shù)運行時間作為一個例子。
import time
def cal():
res = 0
for i in range(100):
res+=i
time.sleep(1)
print("函數(shù)的運行結(jié)果為",res)
return res
def test(func):
print(func)
start_time = time.time()
func()
stop_time = time.time()
print("函數(shù)運行的時間為",stop_time-start_time )
test(cal)
來看下這個函數(shù)運行的結(jié)果:
那這時候,我們不但沒有修改原函數(shù)的源代碼,而且還為函數(shù)添加了一個新的功能,我們的功能就實現(xiàn)了,裝飾器就這么結(jié)束了。難道真的結(jié)束了嗎?你注意到了沒,函數(shù)的方式已經(jīng)發(fā)生改變了,初始我們調(diào)用函數(shù)使用cal(),而現(xiàn)在卻是test(cal),這就違反了我們上述所說的裝飾器的第二原則。
那么我們來看下高階函數(shù)第二種定義:返回值是一個函數(shù)名
import time
def cal():
res = 0
for i in range(100):
res+=i
time.sleep(1)
print("函數(shù)的運行結(jié)果為",res)
return res
def test(func):
start_time = time.time()
cal()
stop_time = time.time()
print("函數(shù)的運行時間為",stop_time-start_time)
return func
cal = test(cal)
# print(res)
cal()
通過傳入?yún)?shù),能夠讓我們避免對源代碼的修改,但修改了函數(shù)的調(diào)用方式
通過返回值為函數(shù)名,能夠讓我們避免修改函數(shù)的調(diào)用方式
這時我們通過對添加功能函數(shù)的參數(shù)傳入為函數(shù),返回值為函數(shù)名,此時,我們滿足了裝飾器的兩個原則。我們這種貌似是可取的,其實不然。我們可以發(fā)現(xiàn)在添加功能時,需要對函數(shù)執(zhí)行一次才可以,這不符合我們的要求。
因此,僅僅只有高階函數(shù)并不能滿足于對我們的需求,不知道還是否記得開頭寫的對裝飾器的知識儲備。裝飾器=高階函數(shù)+函數(shù)嵌套+閉包。接下來,我們來了解什么時閉包吧。
4.閉包
- 閉包的理解 :有很多小伙伴都不懂什么是什么,比較模糊,其實我們可以理解閉包為一種特殊的嵌套函數(shù),這個函數(shù)由兩個函數(shù)嵌套組成,那么在結(jié)構(gòu)上就有外函數(shù)和內(nèi)函數(shù)的說法。 外函數(shù)返回值是內(nèi)函數(shù)的引用結(jié)果
-
(1)外層函數(shù)out_func可以調(diào)用內(nèi)層函數(shù)in_func,但無法引用in_func內(nèi)部的變量y
-
(2)內(nèi)層函數(shù)in_func可以引用外層函數(shù)out_func的變量x
def out_func():
x = 1
def in_func(y):
return x+y
return in_func
test = out_func()
print(test(10))
- test是閉包函數(shù),自由變量是x,因為它不是in_func這個函數(shù)內(nèi)部的變量,但是卻被in_func引用。test的結(jié)果是函數(shù)in_func的地址,那么通過test()便可調(diào)用函數(shù)in_test.
- 每一層的函數(shù),我們可以成為一個包,而閉就是封裝的意思,它封裝的是變量,嵌套在函數(shù)內(nèi)的函數(shù)等價于一個變量,遵循函數(shù)即變量的原則。其實閉包有點類似于作用域,若最里層需要一個變量,我們就可以在當(dāng)前層定義,如果當(dāng)前層不允許,則往上一層,一層一層的往外,也就是說我們可以在最外層定義一個變量,那就可以滲透到最里層
- 閉包的最大用處也是用于裝飾器,接下來,讓我們看看裝飾器的作用吧。
5.裝飾器的實現(xiàn)
-
我們繼續(xù)以上面的例子來了解裝飾器,我們要為一個函數(shù)計算運行時間。
來,先看一段代碼吧。
import time
def timer(func):
def wrapper():
# print(func)
start_time = time.time()
func()
stop_time = time.time()
print(stop_time-start_time)
return wrapper
#@timer #等價于 cal = timer(cal)
def cal():
res = 0
for i in range(100):
res+=i
time.sleep(1)
print("函數(shù)的運行結(jié)果為",res)
return res
cal = timer(cal)
cal()
事到如此,我們基本 完成了裝飾器的功能了,我們不但沒有修改函數(shù)的源代碼,也沒有修改其調(diào)用方式,還為它添加了一個新的功能。
不過,這樣子還存在這一丟丟瑕疵,因為每次給函數(shù)添加功能是都需要做一次賦值操作,那能不能不每次調(diào)用都賦值一次? 那就要用到了一個 @,這是python提供的一個功能。
@timer 相當(dāng)于 cal = timer(cal),我們要修飾那個函數(shù),就在那個函數(shù)前添加@timer
6.含返回值的裝飾器
- 上面的樣子看上去似乎已經(jīng)滿足了我們的要求了,但好像還有點小問題額。。。如果我們需要用到cal函數(shù)的返回結(jié)果的時候,這貌似不符合我們的心意啊。
res = cal()
print("這是函數(shù)的返回結(jié)果的cal:",res)
- 仔細研究一下,cal經(jīng)過裝飾后,本質(zhì)上就是上面的經(jīng)過升級后的wrapper函數(shù),而這個函數(shù)是沒有返回值的,因此,cal()運行的結(jié)果返回就是默認的None啦。
def wrapper():
start_time = time.time()
func()
stop_time = time.time()
print(stop_time-start_time)
- 來看看這一段wrapper函數(shù),我們需要func的返回值,那么就需要在wrapper函數(shù)加上return返回咯,返回的值是func的結(jié)果,那就簡單了,吧func返回的結(jié)果賦值給變量res,然后return返回res這樣就解決啦。
import time
def timer(func):
def wrapper():
# print(func)
start_time = time.time()
res = func()
stop_time = time.time()
print(stop_time-start_time)
return res
return wrapper
@timer
def cal():
res = 0
for i in range(100):
res+=i
time.sleep(1)
print("函數(shù)的運行結(jié)果為",res)
return res
res = cal()
print("這是函數(shù)的返回結(jié)果的cal:",res)
7.含參數(shù)的裝飾器
上面的例子,我們的cal函數(shù)都是不帶參數(shù)的,但是,我們并不能保證以后我們所寫的每一個函數(shù)都是不帶參數(shù)的。
- 舉個例子吧,我們在原來的基礎(chǔ)上定義了一個test1函數(shù):
def timer(func):
def wrapper():
# print(func)
start_time = time.time()
res = func()
stop_time = time.time()
return res
return wrapper
@timer
def cal():
res = 0
for i in range(100):
res+=i
time.sleep(1)
print("函數(shù)的運行結(jié)果為",res)
return res
@timer
def test1(name):
time.sleep(1)
print("這是%s"%(name))
return res
我們在不修改裝飾器的情況下,就會報錯了,wrapped函數(shù)不需要函數(shù),但是一個給出了。
那么我們就要在wrapped函數(shù)中加上參數(shù)name,不過,依然不能解決問題。
這是我們就需要用到了可變參數(shù)了。 如果我們不確定要往函數(shù)中傳入多少個參數(shù),或者我們想往函數(shù)中以列表和元組的形式傳參數(shù)時,那就使要用*args; 如果我們不知道要往函數(shù)中傳入多少個關(guān)鍵詞參數(shù),或者想傳入字典的值作為關(guān)鍵詞參數(shù)時,那就要使用**kwargs。
def timer(func):
def wrapper(*args,**kwargs):
# print(func)
start_time = time.time()
res = func(*args,**kwargs)
stop_time = time.time()
return res
return wrapper
這里我們對wrapper函數(shù)進行修改,在wrapper中就如位置可變參數(shù)和關(guān)鍵字可變參數(shù),這樣的話,我們就不用再擔(dān)心被修飾函數(shù)有多少個參數(shù),以及我們定義的裝飾器可以修飾任何函數(shù)啦。
好啦,裝飾器就到此結(jié)束了。。。。。。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
