粗淺聊聊Python裝飾器

鹹魚彬發表於2021-05-01

淺析裝飾器

通常情況下,給一個物件新增新功能有三種方式:

  • 直接給物件所屬的類新增方法;
  • 使用組合;(在新類中建立原有類的物件,重複利用已有類的功能)
  • 使用繼承;(可以使用現有類的,無需重複編寫原有類進行功能上的擴充套件)

一般情況下,優先使用組合,而不是繼承。但是裝飾器屬於第四種,動態的改變物件從而擴充套件物件的功能。
一般裝飾器的應用場景有列印日誌,效能測試,事務處理,許可權校驗;

Python 內建裝飾器的工作原理

理解Python裝飾器工作原理,首先需要理解閉包這一概念。閉包指的是一個函式巢狀一個函式,內部巢狀的函式呼叫外部函式的
變數,外部函式返回內嵌函式,這樣的結構就是閉包。

裝飾器就是閉包的一種應用,但是裝飾器引數傳遞的是函式。

簡單的閉包示例:

def add_num(x):
    def sum_num(y):
        return x+y
    return sum_num

add_num5 = add_num(5)
total_num = add_num5(100)
print(total_num)

註解:

  • add_num外函式接受引數x,返回內函式sum_num,而內函式sum_num接受引數y,將和add_num外函式接受引數x相加,返回結果。add_num5物件則是定義了add_num外函式接受的引數x為5,最後add_num5(100)返回的結果則是105。

裝飾器的基本使用

簡單計算函式執行時間裝飾器示例:

def times_use(func):
    def count_times(*args, **kwargs):
        start = time.time()
        result  = func(*args, **kwargs)
        end = time.time()
        print(end-start)
        return result
    return count_times

@times_use
def test_decorator():
    time.sleep(2)
    print("Test Decorator")
test_decorator()

註解:

  • 這裡將函式test_decorator作為引數,傳入到times_use函式中,然後內部函式count_times則是會保留原有test_decorator函式程式碼邏輯,在執行test_decorator前儲存執行前時間,然後和執行後的時間進行比較,得出相應的耗時。
  • 通過裝飾器的好處則是能在保留原有函式的基礎上,不用進行對原有函式的修改或者增加新的封裝,就能修飾函式增加新的功能。

根據日誌等級列印日誌裝飾器示例(帶引數的裝飾器):

def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%s is running"% func.__name__)
            result = func(*args, **kwargs)
            print(result) 
            return result
        return wrapper
    return decorator

@use_logging("warn")
def test_decorator():
    print("Test Decorator")
    return "Success"
test_decorator()

計算函式執行時間的類裝飾器示例:

class logTime:
    def __init__(self, use_log=False):
        self._use_log = use_log

    def __call__(self, func):

        def _log(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            print(result)
            end_time = time.time()
            if self._use_log:
                print(end_time-start_time)
            return result
        return _log

    
@logTime(True)
def test_decorator():
    time.sleep(2)
    print("Test Decorator")
    return "Success"

functools wraps使用場景

使用裝飾器雖然能在儲存原有程式碼邏輯的基礎上擴充套件功能,但是原有函式中的元資訊會丟失,比如__name__, __doc__,引數列表。針對這種情況
可以使用functools.wraps,wraps也是一個裝飾器,但是會將原函式的元資訊拷貝到裝飾器函式中。

具體使用方法:

from functools import wraps

def use_logging(level):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):

            if level == "warn":
                logging.warn("%s is running"% func.__name__)
            result = func(*args, **kwargs)
            print(result) 
            return result
        return wrapper
    return decorator

@use_logging("warn")
def test_decorator():
    """" Test Decorator DocString""""
    time.sleep(2)
    print("Test Decorator")
    return "Success"

print(test_decorator.__name__)
print(test_decorator.__doc__)

註解:

  • wraps裝飾器將傳入的test_decorator函式中的元資訊拷貝到wrapper這個裝飾器函式中,使得wrapper擁有和test_decorator的
    元資訊。

關於裝飾器的執行順序

在日常業務中經常會使用多個裝飾器,比如許可權驗證,登入驗證,日誌記錄,效能檢測等等使用場景。所以在使用多個裝飾器
時,就會涉及到裝飾器執行順序的問題。先說結論,關於裝飾器執行順序,可以分為兩個階段:(被裝飾函式)定義階段、(被裝飾函式)執行階段。

  • 函式定義階段,執行順序時從最靠近函式的裝飾器開始,從內向外的執行;
  • 函式執行階段,執行順序時從外而內,一層層的執行;

多裝飾器示例:

def decorator_a(func):
    print("Get in Decorator_a")

    def inner_a(*args, **kwargs):
        print("Get in Inner_a")
        result = func(*args, **kwargs)
        return result
    return inner_a

def decorator_b(func):
    print("Get in Decorator_b")

    def inner_b(*args, **kwargs):
        print("Get in Inner_b")
        result = func(*args, **kwargs)
        return result
    return inner_b

@decorator_b    
@decorator_a
def test_decorator():
    """test decorator DocString"""
    print("Test Decorator")
    return "Success"

執行結果:

Get in Decorator_a
Get in Decorator_b
Get in Inner_b
Get in Inner_a
Test Decorator

程式碼註解:

  • 上述函式使用裝飾器可以相當於decorator_b(decorator_a(test_decorator()),即test_dcorator函式作為引數傳入到decorator_a函式中,然後列印"Get in Decorator_a",並且返回inner_a函式,給上層decorator_b函式,decorator_b函式接受了作為引數的inner_a函式,列印"Get in Decorator_b",然後返回inner_b函式;
  • 此時test_decorator(),即呼叫了該inner_b函式,inner_b函式列印"Get in inner_b",然後呼叫inner_a函式,inner_a列印了"Get in Decorator_a",最後呼叫test_decorator函式。這樣從最外層看,就像直接呼叫了test_decorator函式一樣,但是可以在剛剛的過程中實現功能的擴充套件;

參考連結

https://www.zhihu.com/question/26930016
https://segmentfault.com/a/1190000007837364
https://blog.csdn.net/u013411246/article/details/80571462

相關文章