粗淺聊聊Python裝飾器

淺析裝飾器

通常情況下,給一個對象添加新功能有三種方式:

  • 直接給對象所屬的類添加方法;
  • 使用組合;(在新類中創建原有類的對象,重複利用已有類的功能)
  • 使用繼承;(可以使用現有類的,無需重複編寫原有類進行功能上的擴展)

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

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函數一樣,但是可以在剛剛的過程中實現功能的擴展;

參考鏈接

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

Tags: