【python】高級特性之裝飾器

【摘要】上篇博文介紹了閉包,閉包還有一個常用場景就是裝飾器。因此,本篇博文將介紹裝飾器

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章