python裝飾器的基本流程

[toc]

裝飾器是python的一種語法模式,本質(zhì)上是一種“函數(shù)的函數(shù)”。裝飾器的主要目的是增強(qiáng)函數(shù)的功能,當(dāng)多個函數(shù)需要重復(fù)使用同一個功能的時候,裝飾器可以更加簡介的進(jìn)行實(shí)現(xiàn)。裝飾器的底層邏輯是 函數(shù)的閉包,但這篇文章并不會涉及到。下面的內(nèi)容將會從一個最簡單、常見的案例出發(fā),逐步推導(dǎo)裝飾器的實(shí)現(xiàn)過程。

函數(shù) 是python中最基本的語法單元,假設(shè)有這么一個需求:

  • 需要統(tǒng)計每個函數(shù)的消耗時間
  • 需要在函數(shù)運(yùn)行時打印函數(shù)名
  • 增強(qiáng)函數(shù)功能不能影響函數(shù)原有的參數(shù)和返回值
  • 需要對消耗時間做邏輯判斷,如超過多少秒就記錄下來

如果沒有裝飾器該如何實(shí)現(xiàn)

在沒有裝飾器的前提下,對一個函數(shù)統(tǒng)計其運(yùn)行時間最簡單的辦法是直接修改原函數(shù)內(nèi)部代碼,比如在開始和結(jié)束后分別記錄時間戳并進(jìn)行相減運(yùn)算。這種模式的劣勢在于無法通用,每個函數(shù)都得增加重復(fù)代碼塊,且不便于以后的修改。那么很自然的邏輯是把“統(tǒng)計耗時”這個功能的代碼塊抽象出一個獨(dú)立的方法,如下:

import time


def timer(func):
    start_time = time.time()
    func()
    end_time = time.time()
    print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")


def func():
    print('執(zhí)行fun 函數(shù)...')
    time.sleep(1)


timer(func)

用簡易裝飾器如何實(shí)現(xiàn)

這種方法雖然不必要在每個函數(shù)進(jìn)行增加重復(fù)的代碼,但卻需要在函數(shù)的調(diào)用處修改,將原函數(shù)包裹起來。同樣,這種方法也不是很好的方式。那么,接下來的方法是,將函數(shù)作為參數(shù)傳入到另一個函數(shù)中去,經(jīng)過添加功能的修飾后,再把修飾后的復(fù)合函數(shù)返回出來作為一個新的復(fù)合函數(shù),這樣函數(shù)調(diào)用的時候,用這個新的復(fù)合函數(shù)即可。類似于給一顆糖包裹上一層糖衣。如下:

import time


def timer(func):
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
    return wrapper


def func():
    print('執(zhí)行fun 函數(shù)...')
    time.sleep(1)


var = timer(func)
var()
# timer(func)() 上述兩行可以等價于這種語法
執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.0023250579833984

這種把函數(shù)作為參數(shù)的方式傳入,經(jīng)過修飾后再返回的過程就是python裝飾器的基本模型。但如上代碼所示,這種方式看起來也不是簡潔的,每次調(diào)用的時候還得使用裝飾函數(shù)包裹一次,再用新的函數(shù)執(zhí)行。所以在python中這一步驟用@的語法糖進(jìn)行替代。

import time


def timer(func):
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
    return wrapper


@timer
def func():
    print('執(zhí)行fun 函數(shù)...')
    time.sleep(1)


func()
執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.0023250579833984

如上,函數(shù)fun新增的功能通過另外一個裝飾性函數(shù)實(shí)現(xiàn)每次調(diào)用核心函數(shù)的時候,因?yàn)檠b飾器語法糖的存在,總是先將核心函數(shù)作為參數(shù)傳入裝飾函數(shù)中,執(zhí)行修飾形后代碼邏輯。相比最初的思路,這種方式簡潔、閱讀性好,且不需要改動核心函數(shù)的邏輯。到此,python裝飾器的最簡過程已經(jīng)完成。

如果核心函數(shù)有返回值怎么辦

上述的模型中,核心函數(shù)是一個沒有輸入和輸出的函數(shù)。但如果核心函數(shù)有輸出,即返回值的時候,因?yàn)楹瘮?shù)生命周期的存在,經(jīng)過修飾后的復(fù)合函數(shù)并沒有接收到核心函數(shù)的返回值。所以如果用上述的最簡裝飾模型測試,調(diào)用函數(shù)的返回值為None。

所以,當(dāng)核心函數(shù)有返回值時,必須在裝飾函數(shù)的內(nèi)部也將返回值傳遞出來給復(fù)合函數(shù)。如下:

import time


def timer(func):
    def wrapper():
        start_time = time.time()
        result = func()
        end_time = time.time()
        print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
        return result  # 將核心函數(shù)的輸出值,丟給裝飾函數(shù)保管
    return wrapper


@timer
def func():
    print('執(zhí)行fun 函數(shù)...')
    time.sleep(1)
    return 1024  # 核心函數(shù)有輸出


result = func()
print(result)
執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.0048329830169678
1024

如果核心函數(shù)有參數(shù)怎么辦

同理,如果核心函數(shù)不僅有返回值,也有輸入值,即參數(shù),那么修飾函數(shù)也必須接受該參數(shù)的輸入。但在案例中,修飾方法接收的是任意函數(shù),無法確定核心函數(shù)的參數(shù)類型。所以用萬能參數(shù)替代 *args, **kwargs 如下:

import time


def timer(func):
    def wrapper(*args, **kwargs):  # 核心函數(shù)攜帶參數(shù),丟給裝飾函數(shù)輸入
        start_time = time.time()
        result = func(*args, **kwargs)  # 核心函數(shù)執(zhí)行過程接受參數(shù)
        end_time = time.time()
        print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
        return result  # 將核心函數(shù)的輸出值,丟給裝飾函數(shù)保管
    return wrapper


@timer
def func(x, y):
    # 帶參數(shù)的核心函數(shù)
    print('執(zhí)行fun 函數(shù)...')
    time.sleep(1)
    return x+y  # 核心函數(shù)有輸出


result = func(1024, 996)
print(f"核心函數(shù)輸出:{result}")
print(f"調(diào)用函數(shù)名稱為:{func.__name__}")
執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.0039029121398926
核心函數(shù)輸出:2020
調(diào)用函數(shù)名稱為:wrapper

在這一過程中,修飾函數(shù)并不關(guān)心核心函數(shù)的具體邏輯,也不關(guān)心參數(shù)是什么形式。所做的只是把核心函數(shù)的參數(shù)一起包裹進(jìn)來,并將核心函數(shù)的返回值打包丟給復(fù)合函數(shù)并丟出來。

關(guān)于復(fù)合函數(shù)的名字

python中函數(shù)有自己的名字,通過func.__name__即可看到。上面的測試代碼顯示,復(fù)合函數(shù)的函數(shù)名是wrapper。這樣的結(jié)果符合代碼邏輯,但不符合業(yè)務(wù)邏輯。例如糖衣包裹的棉花糖應(yīng)該叫棉花糖,而不是叫糖衣。為解決這個問題,可以在修飾器中返回wrapper之前,把wrapper的name重置為func的name。如下:

# 函數(shù)名重置
wrapper.__name__ = function.__name__ 
wrapper.__doc__ = function.__doc__
return wrapper

作為裝飾器的通用功能,python內(nèi)部用另一個裝飾器進(jìn)行修飾來代替上面這兩行代碼。functools.wraps(function)這個內(nèi)置的裝飾器的功能實(shí)際上等價于上訴兩行代碼。

所以上面的裝飾器經(jīng)過再次修飾后,最終的形式如下:

import time
import functools
def timer(func):
    @functools.wraps(func) # 內(nèi)置裝飾器,讓被修飾后的函數(shù)的函數(shù)名與原函數(shù)一致
    def wrapper(*args, **kwargs):  # 核心函數(shù)攜帶參數(shù),丟給裝飾函數(shù)輸入
        start_time = time.time()
        result = func(*args, **kwargs)  # 核心函數(shù)執(zhí)行過程接受參數(shù)
        end_time = time.time()
        print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
        return result  # 將核心函數(shù)的輸出值,丟給裝飾函數(shù)保管
    return wrapper
執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.000427007675171
核心函數(shù)輸出:2020
調(diào)用函數(shù)名稱為:func

至此,案例中的需求已經(jīng)基本滿足。從頭來看完整的過程,即便不考慮底層邏輯,也可以從中發(fā)現(xiàn)一個完整的裝飾器實(shí)現(xiàn)模型:

  1. 需要給核心函數(shù)增加功能
  2. 將核心函數(shù)當(dāng)參數(shù)傳入修飾器
  3. 將核心函數(shù)的參數(shù)也傳入修飾器
  4. 核心函數(shù)的返回值在修飾中被拋出,交給復(fù)合函數(shù)托管
  5. 給核心函數(shù)添加裝飾器語法糖變成一個復(fù)合函數(shù)
  6. 調(diào)用核心函數(shù)實(shí)際上是調(diào)用復(fù)合函數(shù)
  7. 最后解決函數(shù)名的歸屬問題

裝飾器的基本模版如下:


image

如果裝飾器需要定制

上述的需求中還有一個對計時進(jìn)行邏輯判斷的需求,如果對于每個核心函數(shù)的判斷都一樣,那么在裝飾器中直接增加代碼邏輯即可。但如果每個核心函數(shù)的判斷不一樣,或者不同時間的要求不一樣,那么就需要定制裝飾器,把裝飾器再裝飾一下,也就是裝飾器的套娃。

import time
import functools


def timer_super(max_time=1):  # 在原裝飾器上繼續(xù)套娃,增加裝飾器參數(shù)
    def timer(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            end_time = time.time()
            print(f"函數(shù){func.__name__} 消耗時間 {end_time-start_time}")
            if (end_time-start_time) > max_time:
                print(f"超過最大耗時 {max_time}s")
            return result
        return wrapper
    return timer


@timer_super(0.5)
def func(x, y):
    print('執(zhí)行fun 函數(shù)...')
    time.sleep(1)
    return x+y  # 核心函數(shù)有輸出


result = func(1024, 996)
print(f"核心函數(shù)輸出:{result}")
print(f"調(diào)用函數(shù)名稱為:{func.__name__}")

執(zhí)行fun 函數(shù)...
函數(shù)func 消耗時間 1.001241683959961
超過最大耗時 0.5s
核心函數(shù)輸出:2020
調(diào)用函數(shù)名稱為:func

總結(jié)

裝飾器的目的是重構(gòu)重復(fù)且通用代碼塊,使代碼得到簡化,提高閱讀性。上面的案例中,只是最常用的統(tǒng)計函數(shù)運(yùn)行時間。裝飾器還可以應(yīng)用到打印日志、登陸檢查、郵件發(fā)送等等具體的業(yè)務(wù)場景。同時,裝飾器的實(shí)現(xiàn)也不一定是函數(shù)的形式,也可以是裝飾器類。但無論是裝飾器函數(shù)還是裝飾器類,其基本邏輯和模版并沒有很大的區(qū)別。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容