【摘要】上篇博文介紹了閉包,閉包還有一個常用場景就是裝飾器。因此,本篇博文將介紹裝飾器
1.裝飾器
試想一下這個場景,如果我們現在有一份代碼,可以給用戶提供打遊戲、看視頻,但是產品經理突然告知我們要對用戶進行年齡判斷,如果是未成年人,則這兩個程序不對用戶執行;如果是成年人,則執行相應程序。那麼,難道我們要改代碼嗎?
又或者,我們需要在每一次的節日都打印該節日快樂,難道要頻繁更改代碼內容嗎?
在Python中,針對這一情況,我們可以使用裝飾器來予以解決。
1.1 裝飾器的概念
裝飾器本質上是一個函數,該函數用來處理其他函數,它可以讓其他函數在不需要修改代碼的前提下增加額外的功能,裝飾器的返回值也是一個函數對象。它經常用於有切面需求的場景,比如:插入日誌、性能測試、事務處理、緩存、權限校驗等應用場景。
1.2 爲何使用裝飾器
有一些場景,需要頻繁更改邏輯,但是遵循 開放封閉 原則,雖然在這個原則是用於面向對象開發,但是也適用於函數式編程。簡單來說,它規定已經實現的功能代碼不允許被修改,但可以被擴展。即:
封閉:已實現的功能代碼塊
開放:對擴展開發
裝飾器可以幫助我們:
1).快速的在不改變原有函數的基礎上,添加邏輯信息
2).不改變函數的調用方式
來看一個簡單的例子(無參數的函數),假如下面這兩個就是已經實現好的播放視頻以及遊戲代碼塊
def videos():
print("正在播放視頻.....")
def game():
print("正在進入遊戲.....")
現在我們要對未成年人進行限制,並且不能在封裝好的代碼塊中修改。所以我們只能使用裝飾器,而裝飾器其實就是閉包的一個應用場景,那肯定要滿足閉包的三個條件;
age = 13
#1). 外部函數裏面也定義了一個函數
def is_adult(fun):
def wrapper():
if age >= 18:
2).內部函數調用了外部函數的臨時變量
fun()
else:
print("未成年")
# 3).返回的是函數的引用
return wrapper
上面的代碼就是我們所說的裝飾器。那麼如何使用裝飾器呢?
#用裝飾器is_adult裝飾函數videos,並將返回只wrapper賦值給videos函數
videos=is_adult(videos)
#調用videos函數——>實質上就是調用wrapper函數內部代碼
videos()
由於上面age變量的值爲13,因此在調用wrapper函數後,執行了對age的判斷,<18打印了未成年
如果把age變量賦值爲35
age = 35
再調用上面的代碼
videos=is_adult(videos)
videos()
即:
則執行效果如下:
但是這樣裝飾函數,實屬麻煩。python中的語法糖可以給我們提供同樣的作用,幫我們裝飾需要裝飾的函數,其語法如下:(只需要在代碼塊上面添加:@裝飾器名稱)
@is_adult #######語法糖所執行的效果就是:videos = is_adult(videos)
def videos():
print("正在播放視頻.....")
@is_adult #######語法糖所執行的效果就是:game = is_adult(game)
def game():
print("正在進入遊戲.....")
再來一個聽音樂的代碼塊,沒有用裝飾器裝飾
def music():
print("正在播放音樂.....")
我們現在給變量age賦值13,然後依次調用這三個函數,執行效果如下:
如果變量age=19,執行效果如下:
這是最簡單的裝飾器例子,無參數的函數裝飾器。在下面的例子中,我們還需要掌握被裝飾的函數有參數、被裝飾的函數有可變參數和關鍵字參數、被多個裝飾器裝飾的函數,最後總結出通用裝飾器的模板。
2.裝飾器的應用場景
2.1 插入日誌
日誌要顯示的內容有:當前時間、主機名、當前正在運行的程序名稱、運行結果
我們的思路是:利用time模塊獲取當前時間,利用os模塊獲取主機名,再用sys模塊獲取程序名稱,最後對這些字段進行拼接。裝飾器內容如下:
def add_log(fun):
def wrapper(*args, **kwargs):
# 獲取被裝飾的函數的返回值
result = fun(*args, **kwargs)
# 返回當前的字符串格式時間
now_time = time.ctime()
# 獲取主機名 nodename='foundation0.ilt.example.com'
hostname = os.uname().nodename.split('.')[0]
# 獲取運行的程序
process_full_name = sys.argv[0]
process_name = os.path.split(process_full_name)[-1]
# 日誌內容
# 獲取函數名: 函數名.__name__
info ="函數[%s]的運行結果爲%s" %(fun.__name__, result)
log = " ".join([now_time, hostname, process_name, info])
print(log)
return result
return wrapper
def wrapper(*args, **kwargs)這一行代碼中的 *args, **kwargs表示接收函數調用時的任意傳參。其中 *args接收到的是一個元組, **kwargs接收到的是一個字典。
fun(*args, **kwargs)是將接收到的參數解包。
獲取主機名的代碼爲什麼那樣寫,可以看下圖,就會比較明晰:
以及獲取當前正在運行的程序名代碼實現:
裝飾器寫好後,我們來裝飾其他函數
在register函數上面使用語法糖,調用add_log這個裝飾器來裝飾register函數。所作的流程如註釋。即在第42行調用register函數時,其實是將該函數中的參數傳給裝飾器中的wrapper函數,並執行。
定義wrapper函數時,參數 *args, **kwargs接收所有任意參數,接收成功後,調用fun(*args, **kwargs)執行register函數體,函數體是打印參數(name、age、province、gender)的值,並將返回值賦給result。
再執行warpper函數體下面的內容。
3.通用裝飾器的模板
掌握下面這個模板,裝飾器就穩了~只需要在裏面添加內容即可。
def decorate(fun):
def wrapper(*args, **kwargs): # args, kwargs是形參
# 在函數之前做的操作
result = fun(*args, **kwargs) # *args, **kwargs是實參, *args, **kwargs是在解包
# result接收被裝飾函數的返回值;
# 在函數之後添加操作
return result
return wrapper
#@裝飾器的名字
@decorate ===> add=decorate(add) ---> add指向wrapper函數位置
def add():
return 'ok'
#調用被裝飾的函數
add()
4.多個裝飾器裝飾
上面我們看了一個裝飾器裝飾一個函數的例子,如果是多個裝飾器裝飾一個函數,執行流程如何呢?
例子一
這裏有兩個裝飾器,分別爲:1). 判斷用戶是否登錄?2). 判斷用戶是否有權限?
def is_login(fun):
def wrapper1(*args, **kwargs):
print("判斷是否登錄......")
result = fun(*args, **kwargs)
return result
return wrapper1
def is_permission(fun):
def wrapper2(*args, **kwargs):
print("判斷是否有權限......")
result = fun(*args, **kwargs)
return result
return wrapper2
用這兩個裝飾器裝飾函數delete()
@is_login
@is_permission
def delete():
print('正在刪除學生信息')
return '刪除學生信息完畢'
執行被裝飾的delete(),用result接收delete()的返回值,並打印。
result = delete()
print(result)
執行效果如下:
用這個簡單的例子,來分析一下執行流程:
1). delete = is_perssion(delete) # delete實際上是wrapper2
先用下面這個裝飾器is_permission裝飾delete函數,裝飾完之後,delete函數指向wrapper2。
2). delete = is_login(delete) # delete = is_login(wrapper2) # delete實際上是wrapper1
再用上面的裝飾器is_login裝飾delete函數(此時的delete函數其實是wrapper2),裝飾完wrapper2函數後返回給delete,delete函數此時指向wrapper1
上面這兩步是裝飾過程,裝飾完成之後,再調用detele函數
3). delete() —> wrapper1() —> wrapper2() —> delete()
此時調用delete函數其實就相當於調用wrapper1,進入wrapper1函數體內執行print(“判斷是否登錄…”),再執行 result = fun(*args, **kwargs),但其實這個fun指的是wrapper2函數,因此這句調用執行wrapper2函數體。
先執行print(“判斷是否有權限…”),再執行 result = fun(*args, **kwargs)。這個fun纔是真正的delete函數,執行delete函數體內容,並將delete函數的返回值獲取到賦值給wrapper2中的 result,wrapper2函數體執行完畢後將返回值result帶回給wrapper1函數體中的result,執行完wrapper1中的函數體將返回值result帶回給調用語句result = delete()的result,最後打印這個返回值。
嗯,是有點繞,如果分析不出來其執行流程的話,記住以下兩句話:
1.裝飾過程: 函數先被下面的裝飾器裝飾,後被上面的裝飾器裝飾
2.執行過程:上面裝飾器先執行, 下面的裝飾器後執行
例子二
假設我們要執行刪除學生信息的程序,但是要求1)用戶登陸成功 2)用戶擁有權限
下面看一下代碼:
首先系統用戶的信息應該存儲在數據庫中,用戶登陸時獲取到用戶登陸信息,與數據庫中的信息作匹配,這裏我們用字典存儲系統中所有用戶的信息。只有兩個用戶root和admin。
db = {
'root': {
'name': 'root',
'passwd': '123',
'is_super': 1 # 0-不是 1-是
},
'admin': {
'name': 'admin',
'passwd': '456',
'is_super': 0 # 0-不是 1-是
}
}
再用一個空字典,來存儲當前登陸用戶的信息
login_user_session = {}
下面是裝飾器is_login,用來判斷用戶是否登錄, 如果沒有登錄,先登錄
def is_login(fun):
def wrapper1(*args, **kwargs):
if login_user_session:
result = fun(*args, **kwargs)
return result
else:
print("請您登陸".center(50, '*'))
user = input("User: ")
passwd = input('Password: ')
if user in db:
if db[user]['passwd'] == passwd:
login_user_session['username'] = user
print('登錄成功')
# ***** 用戶登錄成功, 執行刪除學生的操作;
result = fun(*args, **kwargs)
return result
else:
print("密碼錯誤")
else:
print("用戶不存在")
return wrapper1
第二個裝飾器是is_permission,用來判斷用戶是否有權限進行刪除操作。
def is_permission(fun):
def wrapper2(*args, **kwargs):
print("判斷是否有權限......")
current_user = login_user_session.get('username')
permissson = db[current_user]['is_super']
if permissson == 1:
result = fun(*args, **kwargs)
return result
else:
print("用戶%s沒有權限" % (current_user))
return wrapper2
用這兩個裝飾器來裝飾函數delete。
@is_login
@is_permission
def delete():
return "正在刪除學生信息"
裝飾過程是:
1). delete = is_permission(delete) # delete = wrapper2
先用裝飾器 is_permission裝飾delete函數,裝飾完後,delete指向wrapper2
2). delete = is_login(delete) # delete = is_login(wrapper2) # delete = wrapper1
再用裝飾器 is_login裝飾delete函數(實質上是wrapper2),裝飾完後,delete指向wrapper1
調用函數:
result = delete()
print(result)
被調用的過程是:
delete( ) ------> wrapper1( ) —> wrapper2( ) —> delete( )
執行delete( ),實際上是執行wrapper1( )的函數體,wrapper1中的fun參數是接收到的wrapper2函數,因此執行wrapper2;warpper2( )函數體中的fun參數是delete函數,執行完後,再一步一步將返回值帶回。最後打印。
執行效果如下:
5.帶參數的裝飾器
如果裝飾器需要傳遞參數, 在原有的裝飾器外面嵌套一個函數即可。
直接看例子,假如我們這裏對用戶登陸有限制,只能本地用戶登陸,不支持遠程用戶登錄,可以通過裝飾器接收參數,來分別執行。
以下爲裝飾器,注意要在外部嵌套一個函數
def auth(type):
def wrapper1(fun):
def wrapper(*args, **kwargs):
if type=='local':
user = input("User:")
passwd = input("Passwd:")
if user == 'root' and passwd == '123':
result = fun(*args, **kwargs)
return result
else:
print("用戶名/密碼錯誤")
else:
print("暫不支持遠程用戶登錄")
return wrapper
return wrapper1
用這個裝飾器auth來裝飾home函數
@auth(type='remote')
def home():
print("這是主頁")
這裏要注意的是:上面我們語法糖的結構是@funName 。
這裏的結構是@funName()。默認跟的不是函數名, 先執行改該函數, 獲取函數返回結果,再 @funNameNew
一定要注意:
@函數名 add=函數名(add)
@函數名( ) @新的函數名
調用函數看效果:
home()
如果auth裝飾器中的參數是local,執行結果爲:
【官方裝飾器案例】
https://wiki.python.org/moin/PythonDecoratorLibrary