1. 程式人生 > 其它 >面試題-python 什麼是裝飾器(decorator )?

面試題-python 什麼是裝飾器(decorator )?

前言

python裝飾器本質上就是一個函式,它可以讓其他函式在不需要做任何程式碼變動的前提下增加額外的功能,裝飾器的返回值也是一個函式物件。
很多python初學者學到面向物件類和方法是一道大坎,那麼python中的裝飾器是你進入Python高階語法大門的一道坎。

計算函式執行時間

假設你寫了幾個函式,有一天領導心血來潮說,你把每個函式的執行時長(結束時間-開始時間)統計下,作為一個python實習生的你可能會這樣寫

原始函式

import time

def func_a():
    print("hello")
    time.sleep(0.5)

def func_b():
    print("world")
    time.sleep(0.8)

if __name__ == '__main__':
    func_a()
    func_b()

新增執行時長

作為一個實習生的你,可能想到的解決辦法如下

import time

def func_a():
    start = time.time()
    print("hello")
    time.sleep(0.5)
    end = time.time()
    print("執行時長:%.4f 秒" % (end-start))

def func_b():
    start = time.time()
    print("world")
    time.sleep(0.8)
    end = time.time()
    print("執行時長:%.4f 秒" % (end-start))

if __name__ == '__main__':
    func_a()
    func_b()

執行結果:

hello
執行時長:0.5009 秒
world
執行時長:0.8008 秒

上面的程式碼雖然滿足了領導的要求,但是如果你寫的函式很多的話,每個函式都這樣去新增,會顯得程式碼很臃腫,有很多重複程式碼。
有一天你邊上的一個python老司機看了下你的程式碼,給你指了條明路:裝飾器

函式裝飾器

裝飾器可以寫成函式式裝飾器,也可以寫成一個類裝飾器,先從簡單的函式裝飾器開始學習。
python裝飾器本質上就是一個函式,它可以讓其他函式在不需要做任何程式碼變動的前提下增加額外的功能,裝飾器的返回值也是一個函式物件。

runtime函式就是一個裝飾器了,它對原函式做了包裝並返回了另外一個函式,額外添加了一些功能。在函式上方使用@語法糖就可以呼叫這個裝飾器了

import time

def runtime(func):
    def wrapper():
        start = time.time()
        f = func()     # 原函式
        end = time.time()
        print("執行時長:%.4f 秒" % (end-start))
        return f
    return wrapper

@runtime
def func_a():
    print("hello")
    time.sleep(0.5)

@runtime
def func_b():
    print("world")
    time.sleep(0.8)

if __name__ == '__main__':
    func_a()
    func_b()

執行結果

hello
執行時長:0.5001 秒
world
執行時長:0.8001 秒

函式帶引數裝飾器

上面的runtime就是一個簡單的裝飾器模型了,但並不強壯,如果函式裡面帶有引數,那就不管用了,並且函式的引數是不固定的,這時候就需要用到*args,**kwargs兩兄弟了

import time

def runtime(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        f = func(*args, **kwargs)     # 原函式
        end = time.time()
        print("執行時長:%.4f 秒" % (end-start))
        return f
    return wrapper

@runtime
def func_a(a):
    print("hello"+a)
    time.sleep(0.5)

@runtime
def func_b(b, c="xx"):
    print("world"+b+c)
    time.sleep(0.8)

if __name__ == '__main__':
    func_a("a")
    func_b("b", c="xxx")

類裝飾器

關於__call__方法,不得不先提到一個概念,就是可呼叫物件(callable),我們平時自定義的函式、內建函式和類都屬於可呼叫物件,
但凡是可以把一對括號()應用到某個物件身上都可稱之為可呼叫物件,判斷物件是否為可呼叫物件可以用函式 callable。
如果在類中實現了__call__方法,那麼例項物件也將成為一個可呼叫物件

import time

class runtime(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        start = time.time()
        f = self.func(*args, **kwargs)     # 原函式
        end = time.time()
        print("執行時長:%.4f 秒" % (end-start))
        return f

@runtime
def func_a(a):
    print("hello"+a)
    time.sleep(0.5)

@runtime
def func_b(b, c="xx"):
    print("world"+b+c)
    time.sleep(0.8)

if __name__ == '__main__':
    func_a("a")
    func_b("b", c="xxx")

裝飾器帶引數

快到年底了,領導說執行的速度先不要太快了,讓客戶先加錢,然後再以正常的速度顯示,那麼現在的需求是讓每個函式的執行時間加50%,該如何實現呢?
這就到了裝飾器的高階語法,裝飾器也需要帶上引數了

函式裝飾器

import time

def runtime(slowly=1):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            start = time.time()
            f = func(*args, **kwargs)     # 原函式
            end = time.time()
            t = end-start
            time.sleep((slowly-1)*t)  # 延遲效果
            new_end = time.time()
            print("執行時長:%.4f 秒" % (new_end-start))
            return f
        return inner_wrapper
    return wrapper

@runtime(1.5)
def func_a(a):
    print("hello"+a)
    time.sleep(0.5)

@runtime(1.5)
def func_b(b, c="xx"):
    print("world"+b+c)
    time.sleep(0.8)

if __name__ == '__main__':
    func_a("a")
    func_b("b", c="xxx")

類裝飾器

import time

class runtime(object):
    def __init__(self, slowly=1):
        self.slowly = slowly

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            start = time.time()
            f = func(*args, **kwargs)     # 原函式
            end = time.time()
            t = end-start
            time.sleep((self.slowly-1)*t)  # 延遲效果
            new_end = time.time()
            print("執行時長:%.4f 秒" % (new_end-start))
            return f
        return wrapper

@runtime(1.5)
def func_a(a):
    print("hello"+a)
    time.sleep(0.5)

@runtime(1.5)
def func_b(b, c="xx"):
    print("world"+b+c)
    time.sleep(0.8)

if __name__ == '__main__':
    func_a("a")
    func_b("b", c="xxx")

使用場景

用哪些地方需要使用裝飾器呢?

  • 如果你用過locust,設定權重會用到@task(1),
  • 如果你用過pytest框架,使用fixture功能的時候經常會用到@pytest.fixture(scope="function")
  • allure裡面可以新增測試步驟 @allure.step('修改購物車')
  • 被大量使用於Flask和Django web框架中,檢查是否被授權去使用一個web應用的端點(endpoint)。如 @login_required
  • 也可以自己寫個裝飾器新增日誌

前面一篇對python裝飾器有了初步的瞭解了,但是還不夠完美,領導看了後又提出了新的需求,希望執行的日誌能顯示出具體執行的哪個函式。

namedoc

__name__用於獲取函式的名稱,__doc__用於獲取函式的docstring內容(函式的註釋)

import time

def func_a(a):
    '''func_a --> hello'''
    print("hello"+a)
    time.sleep(0.5)
    return True

def func_b(b, c="xx"):
    '''func_b --> world'''
    print("world"+b+c)
    time.sleep(0.8)
    return True

if __name__ == '__main__':
    print(func_a.__name__)  # 結果 func_a
    print(func_a.__doc__)   # func_a --> hello
    print(func_b.__name__)  # func_b
    print(func_b.__doc__)   # func_b --> world

裝飾器加函式名稱日誌

在裝飾器裡面新增2行程式碼,列印正在執行函式的名稱和docstring內容

import time

def runtime(func):
    '''runtime decorators'''
    def wrapper(*args, **kwargs):
        '''wrapper inner fuction'''
        print("running function : %s" % func.__name__)   
        print("docstring: %s" % func.__doc__)
        start = time.time()
        f = func(*args, **kwargs)     # 原函式
        end = time.time()
        print("執行時長:%.4f 秒" % (end-start))
        return f
    return wrapper

@runtime
def func_a(a):
    '''func_a --> hello'''
    print("hello"+a)
    time.sleep(0.5)
    return True

@runtime
def func_b(b, c="xx"):
    '''func_b --> world'''
    print("world"+b+c)
    time.sleep(0.8)
    return True

if __name__ == '__main__':
    func_a("a")
    print(func_a.__name__)
    print(func_a.__doc__)

執行結果

running function : func_a
docstring: func_a --> hello
helloa
執行時長:0.5008 秒
wrapper
wrapper inner fuction

從執行的結果可以看出,func_a.__name__執行的結果是wrapper,func_a.__doc__執行的結果是wrapper inner fuction。
也就是說被裝飾後的函式其實已經是另外一個函數了(函式名等函式屬性會發生改變),那這個問題如何解決呢?
這就需要用到functools裡面的一個wraps函數了

functools

當func_a函式被裝飾後,導致了一個副作用:自身的函式屬性和docstring內容變成了wrapper函式的屬性了。
這裡需用到functools裡面的一個wraps的裝飾器來消除這樣的副作用。

import time
from functools import wraps

def runtime(func):
    '''runtime decorators'''
    @wraps(func)
    def wrapper(*args, **kwargs):
        '''wrapper inner fuction'''
        print("running function : %s" % func.__name__)
        print("docstring: %s" % func.__doc__)
        start = time.time()
        f = func(*args, **kwargs)     # 原函式
        end = time.time()
        print("執行時長:%.4f 秒" % (end-start))
        return f
    return wrapper

只需在wrapper函式上加上@wraps(func)即可解決

執行結果

running function : func_a
docstring: func_a --> hello
helloa
執行時長:0.5004 秒
func_a
func_a --> hello

類裝飾器

帶引數的裝飾器,可以寫成類裝飾器

import time
from functools import wraps

class runtime(object):
    '''runtime class decorators'''
    def __init__(self, slowly=1):
        self.slowly = slowly

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            '''wrapper inner fuction'''
            print("running function : %s" % func.__name__)
            print("docstring: %s" % func.__doc__)
            start = time.time()
            f = func(*args, **kwargs)     # 原函式
            end = time.time()
            t = end-start
            time.sleep((self.slowly-1)*t)  # 延遲效果
            new_end = time.time()
            print("執行時長:%.4f 秒" % (new_end-start))
            return f
        return wrapper

@runtime(1.5)
def func_a(a):
    '''func_a --> hello'''
    print("hello"+a)
    time.sleep(0.5)
    return True

@runtime()
def func_b(b, c="xx"):
    '''func_b --> world'''
    print("world"+b+c)
    time.sleep(0.8)
    return True

if __name__ == '__main__':
    func_a("a")
    print(func_a.__name__)
    print(func_a.__doc__)

執行結果

running function : func_a
docstring: func_a --> hello
helloa
執行時長:0.7522 秒
func_a
func_a --> hello
Airtest框架分享