1. 開放封閉原則
什么是開放封閉原則?有的同學問開放,封閉這是兩個反義詞這還能組成一個原則么?這不前后矛盾么?其實不矛盾。開放封閉原則是分情況討論的。
我們的軟件一旦上線之后(比如你的軟件主要是多個函數組成的),那么這個軟件對功能的擴展應該是開放的,比如你的游戲一直在迭代更新,推出新的玩法,新功能。但是對於源代碼的修改是封閉的。你就拿函數舉例,如果你的游戲源代碼中有一個函數是閃躲的功能,那么你這個函數肯定是被多個地方調用的,比如對方扔雷,對方開槍,對方用刀,你都會調用你的閃躲功能,那么如果你的閃躲功能源碼改變了,或者調用方式改變了,當對方發起相應的動作,你在調用你的閃躲功能,就會發生問題。所以,開放封閉原則具體具體定義是這樣:
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}')
版本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}') ......
重復代碼太多了,所以要想解決重復代碼的問題,怎么做?我們是不是學過函數,函數就是以功能為導向,減少重復代碼,好我們繼續整改。
版本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()
但是你這樣寫也是有問題的,你雖然將測試功能的代碼封裝成了一個函數,但是這樣,你只能測試小明同學的的函數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()
你要是像上面那么做,每次測試其他同事的代碼還需要手動改,這樣是不是太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)
這樣我將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}主頁')
你將上面的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()
我們分析一下,代碼,代碼執行到這一行: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()) # None
加上裝飾器之后,他的返回值為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())
借助於內層函數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('太白')
上面那么做,顯然報錯了,為什么? 你的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('太白')
這樣你就實現了,還有一個小小的問題,現在被裝飾函數的形參只是有一個形參,如果要是多個怎么辦?有人說多少個我就寫多少個不就行了,那不行呀,你這個裝飾器可以裝飾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)
這樣利用*的打散與聚合的原理,將這些實參通過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)
如果你想給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)
你看此時我調整了一下位置,你要是不把裝飾器放在上面,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 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 inner
你看我上面的裝飾器,不要打開,他可以不可在套一層:

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 auth
舉例說明:抖音:綁定的是微信賬號密碼。 皮皮蝦:綁定的是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('期待你的內涵神評論')
解決方式:

def auth(x): def auth2(func): def inner(*args, **kwargs): if login_status['status']: ret = func() return ret if x == 'wechat': username = input('請輸入用戶名:').strip() password = input('請輸入密碼:').strip() if username == '太白' and password == '123': login_status['status'] = True ret = func() return ret elif x == 'qq': username = input('請輸入用戶名:').strip() password = input('請輸入密碼:').strip() if username == '太白' and password == '123': login_status['status'] = True ret = func() return ret return inner return auth2 @auth('wechat') def jitter(): print('記錄美好生活') @auth('qq') def pipefish(): print('期待你的內涵神評論')
@auth('wechat') :分兩步:
第一步先執行auth('wechat')函數,得到返回值auth2
第二步@與auth2結合,形成裝飾器@auth2 然后在依次執行。
這樣就是帶參數的裝飾器,參數可以傳入多個,一般帶參數的裝飾器在以后的工作中都是給你提供的, 你會用就行,但是自己也一定要會寫,面試經常會遇到。