一、開頭說兩句
作為一名零基礎轉行剛一年的測試新手來說,深知自己在技術經驗方面落後太多,難免會有急於求成的心態,這也就導致自己在學習新知識時似懂非懂,剛開始學完那會還胸有成竹,一段時間之後卻又忘的一乾二淨,導致我要不停回去複習,還始終不得要領,難以在實踐中靈活運用。
相信有不少同學跟我一樣徘徊躊躇,現在老師給予了我一個給大家分享經驗的機會,我也剛好結合前段時間複習關於Python裝飾器的理解來說下,若有不對的地方,還望各位同學,同行,老師及時指出。
二、裝飾器必知基礎
其實很多知識點沒有牢牢掌握,是因為最最基礎的知識沒有理解透徹導致。這也是我在學習裝飾器時對於自己的評價,所以先讓我們來聊聊學習裝飾器所需要的基礎知識。
1、形參與實參
函式的引數分為形式引數和實際引數,簡稱形參和實參。
- 形參即在定義函式時,括號內宣告的引數。形參本質就是一個變數,用來接收外部傳來的值
- 實參即在呼叫函式時,括號內傳入的值,值可以是常量,變數,表示式或三者的組合
具體使用時又分為位置引數,關鍵字引數和預設引數
def info(name,age,sex='male')
print(f'name:{name} age:{age} sex:{sex}')
info(name='jack',18)
上述示例中,呼叫函式時以key=value形式的就是關鍵字引數,定義函式時name,age為位置引數,sex為預設引數。
注意:
- 呼叫函式時,實參可以是按位置或關鍵字的混合使用,但必須保證關鍵字引數在位置引數後面,且不可以對一個形參重複賦值
- 預設引數的值通常應設為不可變型別
2、可變長度引數*args和**kwargs
引數的長度可變指的是呼叫函式時,實參的個數可以不固定,而在呼叫階段,實參無非是按照位置或者按關鍵字兩種形式,因此就出現了兩種解決方案來處理。
2.1 可變長度的位置引數
如果在最後一個形參名前加*號,那麼在呼叫函式時,溢位的位置實參都會被接受,以元組的形式儲存下來賦值給該形參。
def func(x,y,z=1,*args):
print(x,y,z,args)
func(1,2,3,4,5,6,7)
>>1 2 3 (4,5,6,7)
#這裡起作用的就是*號,相當於溢位的位置引數賦值給了它後面的變數,即args=(4,5,6,7)
2.2 可變長度的關鍵字引數
如果在最後一個形參名前加**號,那麼在呼叫函式時,溢位的關鍵字引數,會以字典的形式儲存下來賦值給形參。
def func(x,**kwargs):
print(x)
print(kwargs)
func(x=1,y=2,z=3)
>>1
>>{'y':2,'z':3}
#同上此時相當於把溢位的關鍵字實參一,y,z都被**接收以字典的形式賦值給kwargs,即kwargs={'y':2,'z':3}
2.3 組合使用
可變引數*args與關鍵字引數kwargs通常是組合在一起使用的,如果一個函式的形參為上述兩種型別,那麼代表該函式可以接收任何形式,任意長度的引數。
def wrapper(*args,**kwargs):
pass
在該函式內部還可以把接受到的實參傳給另一個函式,這在後面推導裝飾器時大有用處。
def func(x,y,z):
print(x,y,z)
def wrapper(*args,**kwargs):
func(*args,**kwargs)
wrapper(1,y=2,z=3)
>>1 2 3
分析:
此處在給wrapper傳參時,其遵循的事函式func的引數規則,第一步,位置引數1被接受,以元組形式儲存下來賦值給args,即args=(1,),關鍵字引數y=2,z=3被**以字典形式接收賦值給kwargs,即kwargs={'y':2,'z':3};第二步,執行func(args,kwargs),即func((1,),{'y':2,'z':3}),等同於func(1,y=2,z=3)。
3、函式物件和閉包
函式物件指的是函式可以被當做"資料"來處理,具體可以分為四個方面的使用
3.1 函式可以被引用
def add(x,y):
return x+y
func = add
func(1,2)
>>3
3.2 函式可以作為容器型別的元素
dic = {'add':add}
>>dic
>>{'add': <function add at 0x100661e18>}
>>dic['add'](1,2)
>>3
3.3 函式可以作為引數傳入另一個函式
def foo(x,y,func):
return fun(x,y)
>>foo(1,2,add)
>>3
3.4 函式的返回值可以是一個函式
def bdd():
return add
func=bdd()
func(1,2)
>>3
3.5 閉包函式有兩個關鍵點
-
"閉":值得時函式定義在另一個函式內即內嵌函式。
-
"包":指的是該函式包含對外層函式作用於變數的引用。
def f1(): x = 1 def f2(): print(x) f2() #此時f2就是內嵌函式,為‘閉’,f2有對外層變數x的引用,為‘包’ #但是我們不想在內部呼叫f2函式該怎麼辦呢 #這個時候函式物件的引用,可以作為返回物件就可以解決,即: def f1(): x = 1 def f2(): print(x) return f2 #注意不能加括號,否者就是返回f2的執行結果,我們需要的是他的記憶體地址以供在外部可以隨時呼叫 f = f1() #此刻變數f接受到的就是f2的記憶體地址
總結:
閉包函式提供了一種新的為函式體傳參的方式,為了給f2傳值,在他的同級作用域給了他一個值,f2在整體縮排,外層再給他巢狀一個函式f1包起來。此時f1從原來的全域性變成了區域性,為了使我們在全域性依然可以呼叫它,通過return函式物件再返回到全域性。
三、什麼是裝飾器
上邊講了這麼多,可能大家有點疑惑怎麼還不介紹裝飾器。不用急,這也是我們在學習中常犯的錯誤,急於求成反而不利於對知識的吸收好消化理解。其實在潛移默化中,我們已經把大部分構成裝飾器的基本知識提到了,只是還未進行歸納整理。下面我們又將重新一步一步推導它的由來。
定義:定義一個函式(類),該函式專門用來為其他函式(物件)新增額外的功能。
裝飾器本質上是一個python函式或類,它可以讓其他函式或類在不需要做任何程式碼修改的前提下增加額外功能,裝飾器的返回值也是一個函式/類物件。它經常用於有切面需求的場景,比如:插入日誌,效能測試,事務處理,快取,許可權校驗等場景。
四、為什麼用裝飾器
我們在為一個物件新增新功能時,往往秉持著開放封閉原則。
- 開放:指的是對擴充功能是開放的
- 封閉:指的是修改原始碼是封閉的
即在不修改被裝飾物件原始碼和呼叫方式的情況下為被裝飾物件新增功能。有了裝飾器,我們就可以抽離出大量與函式功能本身無關的雷同程式碼到裝飾器中並繼續重用。
五、裝飾器的推導
提出需求:為index函式新增計算程式碼執行時間的功能,必須符合開放封閉原則。
import time
def index(x,y):
time.sleep(2)
print(f'來自index的{x}和{y}')
1、方案一
def index(x, y):
start = time.time()
time.sleep(2)
print(f"來自index的{x}和{y}")
stop = time.time()
print(stop-start)
結果:雖然實現了功能,但破環了開放封閉原則,修改了原始碼,不符合要求,失敗。
2、方案二
start = time.time()
index(1, 2)
stop = time.time()
print(stop-start)
結果:上述程式碼雖然沒有修改原始碼,也實現了功能,但是每次使用都要加上這三行程式碼,太過冗餘,失敗。
3、方案三
'''在方案二的基礎上進行優化,為了解決程式碼冗餘,我們把它寫成一個函式'''
def wrapper():
start = time.time()
index(1, 2)
stop = time.time()
print(stop-start)
wrapper()
結果:此時我們不用每次加上三行程式碼,只需呼叫wrapper函式即可,但是複用性依然不夠,可以在進行優化。
4、方案四
優化:解決index的傳參被寫死了的問題
#此時函式引數的知識就用上了
def wrapper(a, b):
start = time.time()
index(a, b)
stop = time.time()
print(stop-start)
wrapper(1,2)
#但是可能在後續的需求中index的傳參個數會發生變化
#此時可變長度引數就能幫上大忙了
def wrapper(*args, **kwargs):
start = time.time()
index(*args, **kwargs)
stop = time.time()
print(stop-start)
'''這個時候就不用擔心給他傳引數的問題了,wrapper收到什麼引數都會原封不動交給index函式'''
結果:進行了一系列的優化,我們發現雖然傳參的問題解決了,但是這個時候index函式也寫死了,以後的需求中不可能只有它需要這個功能,複用性不夠,因此可以繼續優化。
5、方案五
優化:index寫死了的問題
'''我們知道一旦某個變數寫死了,那麼我們就用一個變數去代替他,但是在wrapper函式中,index寫成變數後,無法通過形參傳給他,這個時候閉包函式就大顯神威了,它就提供了一種給函式傳參的方式'''
def outter(func):
#func = index #寫活
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
stop = time.time()
print(stop-start)
return wrapper
f = outter(index)
'''返回的是wrapper的記憶體地址賦值給f,加個括號就是在呼叫wrapper,它的作用就是計算以函式物件傳入其中的index的執行時間統計'''
# 為了不改變呼叫方式,在進行優化
# 可以把f = outter(index),為什麼不可以賦值給index呢
# 最後:index = outter(index) 即wrapper的記憶體地址
index() # 此時對於函式的呼叫者來說,他沒有變化,早就換了
6、方案五
優化:上面看是已經優化得差不多了,其實還是有漏洞,原函式index是沒有返回值的,此時呼叫換掉之後的index之後,返回的時wrapper的記憶體地址,它並沒有返回值,index()返回的是None,沒有做到天衣無縫。這個時候就要用到我們上面講到的函式物件的引用可以作為返回值,問題就迎刃而解了。
def outter(func):
#func = index #寫活
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
stop = time.time()
print(stop-start)
return res
return wrapper
''' 我們把func函式的返回值通過return,在返回出來,當我們執行index()時,實際上就是在呼叫wrapper,此刻它是有返回值的,也就是func的返回值,這個時候才做到了天衣無縫'''
6、最終方案(推薦)
def outter(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return res
return wrapper
這就是一個最簡單的無參裝飾器的模版,我們想要給某個物件也就是func函式新增新功能時直接在wrapper函式內部書寫程式碼即可。
關於有參裝飾器,此處由於篇幅限制就不在說明。有參也就說明我們的函式內部需要一個引數,無非就是兩種方式,一種通過形參直接傳入,另一種就是通過閉包函式直接包給它,此處肯定是利用閉包函式更合理。
六、感言
誤打誤撞,因為老師的一個課後作業任務,完成了本人的第一篇知識總結。剛開始是有點驚慌的,但是隨之而來的是驚喜,雖然擔心寫的不夠好,但也算是想給自己一個交代,一個好的開始。知識的持續分享總結能夠促進我們持續的學習進步。
經過這次小小的分享,回到開頭,我想說的就是學習一些高階知識,當我們感到模模糊糊的時候,不妨迴歸本質,從最基礎的原理對他進行分解,一步一步推導,往往能給到我們一種醍醐灌頂,意想不到的收穫。最後希望同大家一道能通過這次的學習,提升自己,完成自己的初心。
辛丑伊始 奮鬥不止
--黎潘