Python 裝飾器初探

Python 裝飾器初探

在談及Python的時候,裝飾器一直就是道繞不過去的坎。面試的時候,也常常會被問及裝飾器的相關知識。總感受本身的理解很淺顯,不夠深入。是時候作出改變,對Python的裝飾器作個全面的瞭解了。python

1. 函數裝飾器

直接上代碼,看看裝飾器到底幹了些什麼?面試

from functools import wraps
import time


def time_cost(func):
    @wraps(func)
    def f(*args, **kwargs):
        start_time = time.time()
        func(*args, **kwargs)
        end_time = time.time()
        print(end_time - start_time)
    return f


@time_cost
def test(*args, **kwargs):
    time.sleep(1.0)


if __name__ == "__main__":
    test()

上面的Python代碼,運行後,會給出test函數的執行時間。代碼的執行順序大概以下,首先是將test做爲值傳遞給time_cost函數,返回函數f,而後再調用f,這是帶有time_cost裝飾器的test函數的大體執行過程。閉包

從中,不難看出,即便不使用裝飾器符號,咱們利用Python的語言特性,也能達成上述目的。用裝飾器符號的好處是簡化了代碼,增長了代碼的可讀性。app

這是一段很是簡單的對函數使用裝飾器的Python代碼。等等,@wraps(func)是什麼鬼?悄悄幹了什麼哇?函數

咱們稍微修改下上述代碼,結果以下:code

from functools import wraps
import time


def time_cost(func):
    def f(*args, **kwargs):
        start_time = time.time()
        func(*args, **kwargs)
        end_time = time.time()
        print(end_time - start_time)

    print('hello world')
    return f


@time_cost
def test(*args, **kwargs):
    time.sleep(1.0)


if __name__ == "__main__":
    print(test.__name__)

發現輸出了hello world,同時輸出test.__name__,竟然變成了f,並非咱們預期的test。根據這樣的輸出結果,咱們不可貴出,其實被裝飾器time_cost修飾過的函數test本質上已經等同於time_cost(test),此時訪問test.__name__實際上訪問的是time_cost(test).__name__,獲得的固然就是f啦。當咱們加上@wraps(func),此時test.__name__變成了testorm

下面介紹帶參數的裝飾器,更加難了。在談論帶參數的裝飾器之間,首先得引入一個概念,那就」閉包「。若是你之前用過腳本語言,好比JavaScript,那麼必定會很熟悉閉包這個概念。下面是一個閉包樣例對象

def add(a):
    def wrapper(c):
        return a + c
    return wrapper

if __name__ == "__main__":
    add3 = add(3)
    add9 = add(9)
    print(add3(4) == 7)
    print(add9(1) == 10)

從中能夠看出,在調用add3的時候,wrapper內部還能夠訪問到a的值,這就是閉包的做用。理解了閉包,理解帶參數的裝飾器就容易多了。ip

from functools import wraps

def logging(level):
    def outer_wrapper(func):
        @wraps(func)
        def inner_wrapper(*args, **kwargs):
            print("[{level}]: enter function {func}()".format(
                level=level,
                func=func.__name__))
            return func(*args, **kwargs)
        return inner_wrapper
    return outer_wrapper

@logging(level='WARN')
def show(msg):
    print('message:{}'.format(msg))

if __name__ == "__main__":
    show('hello world!')

上面給出了一個帶參數裝飾器的示例。根據咱們前面的鋪墊,咱們不難分析得出,上面的執行過程是logging(level='WARN')->outer_wrapper(show)->inner_wrapper(),因此咱們能夠理解,在被logging修飾後的show其實就是logging(level='WARN')(show),執行show('hello world!')其實就是在執行logging(level='WARN')(show)()。注意與不帶參數的裝飾器的區別,帶參數的裝飾器比不帶參數的裝飾器多套了一層,對應的裝飾器也有了調用。由於在使用裝飾器的時候,帶了括號,因此裝飾器自己多套了一層。被裝飾器修飾過的函數在被調用的時候,實際上執行的是裝飾器最內層的函數,其他層的在函數被修飾時就已經執行了。it

是否是以爲很是天然?對的,我之前對裝飾器的理解也就停留在不帶參數的裝飾器這一深度。

2. 基於類實現的裝飾器

依然先上代碼

from functools import wraps
import time


class time_cost:
    def __init__(self, func):
        self.func = func

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


@time_cost
def test(*args, **kwargs):
    time.sleep(1.0)


if __name__ == "__main__":
    test()

上面的基於類實現的不帶參數的裝飾器實際上利用的是Python中的可調用對象特性,凡是實現了__call__方法的類的實例是能夠被調用的。所以被time_cost修飾過的test函數本質上已經變成了time_cost類的實例了。調用test方法的時候,實際上執行的是__call__方法。

下面介紹稍微複雜一點的基於類實現的帶有參數的裝飾器。

from functools import wraps

class logging:
    def __init__(self, level):
        self.level = level
    
    def __call__(self, func):
        
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("[{level}]: enter function {func}()".format(
                level=self.level,
                func=func.__name__))
            return func(*args, **kwargs)
        return wrapper


@logging(level='WARN')
def show(msg):
    print('message:{}'.format(msg))


if __name__ == "__main__":
    show('hello world!')

不一樣於基於類實現的不帶參數的裝飾器,基於類實現的帶參數的裝飾器在__call__裏面多了一層wrapper。被裝飾器修飾的show方法本質上是logging(level='WARN')(show),此時調用show方法,實際上執行的是wrapper方法。

如今看來,其實裝飾器也沒有很複雜,在實際的項目中用裝飾器能夠帶來很大便利。

相關文章
相關標籤/搜索