Prince and Princess HDU - 4685
<div id="page_begin_html"> <script>loadPageBeginHtml();</script> </div> <!--done-->
太白金星
</div><!--end: blogTitle 部落格的標題和副標題 -->
<div id="navigator">
<div class="blogStats">
<span id="stats_post_count">隨筆 - 6 </span>
文章 - 121
評論 - 239
</div><!--end: blogStats -->
</div><!--end: navigator 部落格導航欄 -->
裝飾器
</h1>
<div class="clear"></div>
<div class="postBody">
1. 開放封閉原則
</h1>
<div class="clear"></div>
<div class="postBody">
什麼是開放封閉原則?有的同學問開放,封閉這是兩個反義詞這還能組成一個原則麼?這不前後矛盾麼?其實不矛盾。開放封閉原則是分情況討論的。
我們的軟體一旦上線之後(比如你的軟體主要是多個函式組成的),那麼這個軟體對功能的擴充套件應該是開放的,比如你的遊戲一直在迭代更新,推出新的玩法,新功能。但是對於原始碼的修改是封閉的。你就拿函式舉例,如果你的遊戲原始碼中有一個函式是閃躲的功能,那麼你這個函式肯定是被多個地方呼叫的,比如對方扔雷,對方開槍,對方用刀,你都會呼叫你的閃躲功能,那麼如果你的閃躲功能原始碼改變了,或者呼叫方式改變了,當對方發起相應的動作,你在呼叫你的閃躲功能,就會發生問題。所以,開放封閉原則具體具體定義是這樣:
1.對擴充套件是開放的
我們說,任何一個程式,不可能在設計之初就已經想好了所有的功能並且未來不做任何更新和修改。所以我們必須允許程式碼擴充套件、新增新功能。
2.對修改是封閉的
就像我們剛剛提到的,因為我們寫的一個函式,很有可能已經交付給其他人使用了,如果這個時候我們對函式內部進行修改,或者修改了函式的呼叫方式,很有可能影響其他已經在使用該函式的使用者。OK,理解了開封封閉原則之後,我們聊聊裝飾器。
什麼是裝飾器?從字面意思來分析,先說裝飾,什麼是裝飾? 裝飾就是新增新的,比如你家剛買的房子,下一步就是按照自己的喜歡的方式設計,進行裝修,裝飾,地板,牆面,家電等等。什麼是器?器就是工具,也是功能,那裝飾器就好理解了:就是新增新的功能。
比如我現在不會飛,怎麼才能讓我會飛?給我加一個翅膀,我就能飛了。那麼你給我加一個翅膀,它會改變我原來的行為麼?我之前的吃喝拉撒睡等生活方式都不會改變。它就是在我原來的基礎上,添加了一個新的功能。
今天我們講的裝飾器(裝修,翅膀)是以功能為導向的,就是一個函式。
被裝飾的物件:比如毛坯房,我本人,其實也是一個函式。
所以裝飾器最終最完美的定義就是:在不改變原被裝飾的函式的原始碼以及呼叫方式下,為其新增額外的功能。
2. 初識裝飾器
接下來,我們通過一個例子來為大家講解這個裝飾器:
需求介紹:你現在xx科技有限公司的開發部分任職,領導給你一個業務需求讓你完成:讓你寫程式碼測試小明同學寫的函式的執行效率。
def index(): print('歡迎訪問部落格園主頁')
版本1:
需求分析:你要想測試此函式的執行效率,你應該怎麼做?應該在此函式執行前記錄一個時間, 執行完畢之後記錄一個時間,這個時間差就是具體此函式的執行效率。那麼執行時間如何獲取呢? 可以利用time模組,有一個time.time()功能。
import time print(time.time())
此方法返回的是格林尼治時間,是此時此刻距離1970年1月1日0點0分0秒的時間秒數。也叫時間戳,他是一直變化的。所以要是計算shopping_car的執行效率就是在執行前後計算這個時間戳的時間,然後求差值即可。
import time def index(): print('歡迎訪問部落格園主頁') start_time = time.time() index() end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}')
由於index函式只有一行程式碼,執行效率太快了,所以我們利用time模組的一個sleep模擬一下
import time def index(): time.sleep(2) # 模擬一下網路延遲以及程式碼的效率 print('歡迎訪問部落格園主頁') start_time = time.time() index() end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}')View Code
版本1分析:你現在已經完成了這個需求,但是有什麼問題沒有? 雖然你只寫了四行程式碼,但是你完成的是一個測試其他函式的執行效率的功能,如果讓你測試一下,小張,小李,小劉的函式效率呢? 你是不是全得複製:
import time def index(): time.sleep(2) # 模擬一下網路延遲以及程式碼的效率 print('歡迎訪問部落格園首頁') def home(name): time.sleep(3) # 模擬一下網路延遲以及程式碼的效率 print(f'歡迎訪問{name}主頁') start_time = time.time() index() end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') start_time = time.time() home('太白') end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') ......View Code
重複程式碼太多了,所以要想解決重複程式碼的問題,怎麼做?我們是不是學過函式,函式就是以功能為導向,減少重複程式碼,好我們繼續整改。
版本2:
import time def index(): time.sleep(2) # 模擬一下網路延遲以及程式碼的效率 print('歡迎訪問部落格園主頁') def inner(): start_time = time.time() index() end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') inner()View Code
但是你這樣寫也是有問題的,你雖然將測試功能的程式碼封裝成了一個函式,但是這樣,你只能測試小明同學的的函式index,你要是測試其他同事的函式呢?你怎麼做?
import time def index(): time.sleep(2) # 模擬一下網路延遲以及程式碼的效率 print('歡迎訪問部落格園主頁') def home(name): time.sleep(3) # 模擬一下網路延遲以及程式碼的效率 print(f'歡迎訪問{name}主頁') def inner(): start_time = time.time() index() home('太白') end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') timer()View Code
你要是像上面那麼做,每次測試其他同事的程式碼還需要手動改,這樣是不是太low了?所以如何變成動態測試其他函式?我們是不是學過函式的傳參?能否將被裝飾函式的函式名作為函式的引數傳遞進去呢?
版本3:
import time def index(): time.sleep(2) # 模擬一下網路延遲以及程式碼的效率 print('歡迎訪問部落格園主頁') def home(name): time.sleep(3) # 模擬一下網路延遲以及程式碼的效率 print(f'歡迎訪問{name}主頁') def timmer(func): # func == index 函式 start_time = time.time() func() # index() end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') timmer(index)View Code
這樣我將index函式的函式名作為引數傳遞給timmer函式,然後在timmer函式裡面執行index函式,這樣就變成動態傳參了。好,你們現在將版本3的程式碼快速練一遍。 大家練習完了之後,發現有什麼問題麼? 對比著開放封閉原則說: 首先,index函式除了完成了自己之前的功能,還增加了一個測試執行效率的功能,對不?所以也符合開放原則。 其次,index函式原始碼改變了麼?沒有,但是執行方式改變了,所以不符合封閉原則。 原來如何執行? index() 現在如何執行? inner(index),這樣會造成什麼問題? 假如index在你的專案中被100處呼叫,那麼這相應的100處呼叫我都得改成inner(index)。 非常麻煩,也不符合開放封閉原則。
版本4:實現真正的開放封閉原則:裝飾器。
這個也很簡單,就是我們昨天講過的閉包,只要你把那個閉包的執行過程整清楚,那麼這個你想不會都難。
import time def index(): time.sleep(2) # 模擬一下網路延遲以及程式碼的效率 print('歡迎訪問部落格園主頁') def home(name): time.sleep(3) # 模擬一下網路延遲以及程式碼的效率 print(f'歡迎訪問{name}主頁')View Code
你將上面的inner函式在套一層最外面的函式timer,然後將裡面的inner函式名作為最外面的函式的返回值,這樣簡單的裝飾器就寫好了,一點新知識都沒有加,這個如果不會就得多抄幾遍,然後理解程式碼。
def timer(func): # func = index def inner(): start_time = time.time() func() end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') return inner # f = timer(index) # f()View Code
我們分析一下,程式碼,程式碼執行到這一行:f = timer(index) 先執行誰?看見一個等號先要執行等號右邊, timer(index) 執行timer函式將index函式名傳給了func形參。內層函式inner執行麼?不執行,inner函式返回 給f變數。所以我們執行f() 就相當於執行inner閉包函式。 f(),這樣既測試效率又執行了原函式,有沒有問題?當然有啦!!版本4你要解決原函式執行方式不改變的問題,怎麼做? 所以你可以把 f 換成 index變數就完美了! index = timer(index) index()帶著同學們將這個流程在執行一遍,特別要注意 函式外面的index實際是inner函式的記憶體地址而不是index函式。讓學生們抄一遍,理解一下,這個timer就是最簡單版本裝飾器,在不改變原index函式的原始碼以及呼叫方式前提下,為其增加了額外的功能,測試執行效率。
3. 帶返回值的裝飾器
你現在這個程式碼,完成了最初版的裝飾器,但是還是不夠完善,因為你被裝飾的函式index可能會有返回值,如果有返回值,你的裝飾器也應該不影響,開放封閉原則嘛。但是你現在設定一下試試:
import time def index(): time.sleep(2) # 模擬一下網路延遲以及程式碼的效率 print('歡迎訪問部落格園主頁') return '訪問成功' def timer(func): # func = index def inner(): start_time = time.time() func() end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') return inner index = timer(index) print(index()) # NoneView Code
加上裝飾器之後,他的返回值為None,為什麼?因為你現在的index不是函式名index,這index實際是inner函式名。所以index() 等同於inner() 你的 '訪問成功'返回值應該返回給誰?應該返回給index,這樣才做到開放封閉,實際返回給了誰?實際返回給了func,所以你要更改一下你的裝飾器程式碼,讓其返回給外面的index函式名。 所以:你應該這麼做:
def timer(func): # func = index def inner(): start_time = time.time() ret = func() end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') return ret return inner index = timer(index) # inner print(index()) # print(inner())View Code
藉助於內層函式inner,你將func的返回值,返回給了inner函式的呼叫者也就是函式外面的index,這樣就實現了開放封閉原則,index返回值,確實返回給了'index'。
讓同學們;練習一下。
4.4 被裝飾函式帶引數的裝飾器
到目前為止,你的被裝飾函式還是沒有傳參呢?按照我們的開放封閉原則,加不加裝飾器都不能影響你被裝飾函式的使用。所以我們看一下。
import time def index(): time.sleep(2) # 模擬一下網路延遲以及程式碼的效率 print('歡迎訪問部落格園主頁') return '訪問成功' def home(name): time.sleep(3) # 模擬一下網路延遲以及程式碼的效率 print(f'歡迎訪問{name}主頁') def timer(func): # func = index def inner(): start_time = time.time() func() end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') return inner # 要想timer裝飾home函式怎麼做? home = timer(home) home('太白')View Code
上面那麼做,顯然報錯了,為什麼? 你的home這個變數是誰?是inner,home('太白')實際是inner('太白')但是你的'太白'這個實參應該傳給誰? 應該傳給home函式,實際傳給了誰?實際傳給了inner,所以我們要通過更改裝飾器的程式碼,讓其將實參'太白'傳給home.
import time def index(): time.sleep(2) # 模擬一下網路延遲以及程式碼的效率 print('歡迎訪問部落格園主頁') return '訪問成功' def home(name): time.sleep(3) # 模擬一下網路延遲以及程式碼的效率 print(f'歡迎訪問{name}主頁') def timer(func): # func = home def inner(name): start_time = time.time() func(name) # home(name) == home('太白') end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') return inner # 要想timer裝飾home函式怎麼做? home = timer(home) home('太白')View Code
這樣你就實現了,還有一個小小的問題,現在被裝飾函式的形參只是有一個形參,如果要是多個怎麼辦?有人說多少個我就寫多少個不就行了,那不行呀,你這個裝飾器可以裝飾N多個不同的函式,這些函式的引數是不統一的。所以你要有一種可以接受不定數引數的形參接受他們。這樣,你就要想到*args,**kwargs。
import time def index(): time.sleep(2) # 模擬一下網路延遲以及程式碼的效率 print('歡迎訪問部落格園主頁') return '訪問成功' def home(name,age): time.sleep(3) # 模擬一下網路延遲以及程式碼的效率 print(name,age) print(f'歡迎訪問{name}主頁') def timer(func): # func = home def inner(*args,**kwargs): # 函式定義時,*代表聚合:所以你的args = ('太白',18) start_time = time.time() func(*args,**kwargs) # 函式的執行時,*代表打散:所以*args --> *('太白',18)--> func('太白',18) end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') return inner home = timer(home) home('太白',18)View Code
這樣利用*的打散與聚合的原理,將這些實參通過inner函式的中間完美的傳遞到給了相應的形參。
好將上面的程式碼在敲一遍。
5. 標準版裝飾器
程式碼優化:語法糖
根據我的學習,我們知道了,如果想要各給一個函式加一個裝飾器應該是這樣:
def home(name,age): time.sleep(3) # 模擬一下網路延遲以及程式碼的效率 print(name,age) print(f'歡迎訪問{name}主頁') def timer(func): # func = home def inner(*args,**kwargs): start_time = time.time() func(*args,**kwargs) end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') return inner home = timer(home) home('太白',18)View Code
如果你想給home加上裝飾器,每次執行home之前你要寫上一句:home = timer(home)這樣你在執行home函式 home('太白',18) 才是真生的添加了額外的功能。但是每次寫這一句也是很麻煩。所以,Python給我們提供了一個簡化機制,用一個很簡單的符號去代替這一句話。
def timer(func): # func = home def inner(*args,**kwargs): start_time = time.time() func(*args,**kwargs) end_time = time.time() print(f'此函式的執行效率為{end_time-start_time}') return inner @timer # home = timer(home) def home(name,age): time.sleep(3) # 模擬一下網路延遲以及程式碼的效率 print(name,age) print(f'歡迎訪問{name}主頁') home('太白',18)View Code
你看此時我調整了一下位置,你要是不把裝飾器放在上面,timer是找不到的。home函式如果想要加上裝飾器那麼你就在home函式上面加上@home,就等同於那句話 home = timer(home)。這麼做沒有什麼特殊意義,就是讓其更簡單化,比如你在影視片中見過野戰軍的作戰時由於不方便說話,用一些簡單的手勢代表一些話語,就是這個意思。
至此標準版的裝飾器就是這個樣子:
def wrapper(func): def inner(*args,**kwargs): '''執行被裝飾函式之前的操作''' ret = func '''執行被裝飾函式之後的操作''' return ret return inner
這個就是標準的裝飾器,完全符合程式碼開放封閉原則。這幾行程式碼一定要背過,會用。
此時我們要利用這個裝飾器完成一個需求:簡單版模擬部落格園登入。此時帶著學生們看一下部落格園,說一下需求: 部落格園登陸之後有幾個頁面,diary,comment,home,如果我要訪問這幾個頁面,必須驗證我是否已登入。 如果已經成功登入,那麼這幾個頁面我都可以無阻力訪問。如果沒有登入,任何一個頁面都不可以訪問,我必須先登入,登入成功之後,才可以訪問這個頁面。我們用成功執行函式模擬作為成功訪問這個頁面,現在寫三個函式,寫一個裝飾器,實現上述功能。
def auth(): pass View Code
def diary():
print('歡迎訪問日記頁面')
def comment():
print('歡迎訪問評論頁面')
def home():
print('歡迎訪問部落格園主頁')
答案:login_status = {
'username': None,
'status': False,
}
def auth(func):
def inner(*args,**kwargs):
if login_status['status']:
ret = func()
return ret
username = input('請輸入使用者名稱:').strip()
password = input('請輸入密碼:').strip()
if username == '太白' and password == '123':
login_status['status'] = True
ret = func()
return ret
return inner
@auth
def diary():
print('歡迎訪問日記頁面')
@auth
def comment():
print('歡迎訪問評論頁面')
@auth
def home():
print('歡迎訪問部落格園主頁')
diary()
comment()
home()
6. 帶引數的裝飾器
我們看,裝飾器其實就是一個閉包函式,再說簡單點就是兩層的函式。那麼是函式,就應該具有函式傳參功能。
login_status = { 'username': None, 'status': False, } def auth(func): def inner(*args,**kwargs): if login_status['status']: ret = func() return ret username = input('請輸入使用者名稱:').strip() password = input('請輸入密碼:').strip() if username == '太白' and password == '123': login_status['status'] = True ret = func() return ret return innerView Code
你看我上面的裝飾器,不要開啟,他可以不可在套一層:
def auth(x): def auth2(func): def inner(*args,**kwargs): if login_status['status']: ret = func() return ret username = input('請輸入使用者名稱:').strip() password = input('請輸入密碼:').strip() if username == '太白' and password == '123': login_status['status'] = True ret = func() return ret return inner return authView Code
舉例說明:抖音:繫結的是微信賬號密碼。 皮皮蝦:繫結的是qq的賬號密碼。 你現在要完成的就是你的裝飾器要分情況去判斷賬號和密碼,不同的函式用的賬號和密碼來源不同。 但是你之前寫的裝飾器只能接受一個引數就是函式名,所以你寫一個可以接受引數的裝飾器。
def auth2(func): def inner(*args, **kwargs): if login_status['status']: ret = func() return ret if 微信: username = input('請輸入使用者名稱:').strip() password = input('請輸入密碼:').strip() if username == '太白' and password == '123': login_status['status'] = True ret = func() return ret elif 'qq': username = input('請輸入使用者名稱:').strip() password = input('請輸入密碼:').strip() if username == '太白' and password == '123': login_status['status'] = True ret = func() return ret return inner @auth2 def jitter(): print('記錄美好生活') @auth2 def pipefish(): print('期待你的內涵神評論')View Code
解決方式:
def auth(x): def auth2(func): def inner(*args, **kwargs): if login_status['status']: ret = func() return retView Code</span><span style="color: rgba(0, 0, 255, 1)">if</span> x == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">wechat</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">: username </span>= input(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">請輸入使用者名稱:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">).strip() password </span>= input(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">請輸入密碼:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">).strip() </span><span style="color: rgba(0, 0, 255, 1)">if</span> username == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">太白</span><span style="color: rgba(128, 0, 0, 1)">'</span> <span style="color: rgba(0, 0, 255, 1)">and</span> password == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">123</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">: login_status[</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">status</span><span style="color: rgba(128, 0, 0, 1)">'</span>] =<span style="color: rgba(0, 0, 0, 1)"> True ret </span>=<span style="color: rgba(0, 0, 0, 1)"> func() </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> ret </span><span style="color: rgba(0, 0, 255, 1)">elif</span> x == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">qq</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">: username </span>= input(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">請輸入使用者名稱:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">).strip() password </span>= input(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">請輸入密碼:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">).strip() </span><span style="color: rgba(0, 0, 255, 1)">if</span> username == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">太白</span><span style="color: rgba(128, 0, 0, 1)">'</span> <span style="color: rgba(0, 0, 255, 1)">and</span> password == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">123</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">: login_status[</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">status</span><span style="color: rgba(128, 0, 0, 1)">'</span>] =<span style="color: rgba(0, 0, 0, 1)"> True ret </span>=<span style="color: rgba(0, 0, 0, 1)"> func() </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> ret </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> inner </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> auth2
@auth('wechat')
def jitter():
print('記錄美好生活')
@auth('qq')
def pipefish():
print('期待你的內涵神評論')
@auth('wechat') :分兩步:
第一步先執行auth('wechat')函式,得到返回值auth2
第二步@與auth2結合,形成裝飾器@auth2 然後在依次執行。
這樣就是帶引數的裝飾器,引數可以傳入多個,一般帶引數的裝飾器在以後的工作中都是給你提供的, 你會用就行,但是自己也一定要會寫,面試經常會遇到。