1. 程式人生 > >Python Decorator(裝飾器)

Python Decorator(裝飾器)

今天來說說 Python 裡的裝飾器 (decorator)。它不難,但卻幾乎是 “精通” Python 的路上的第一道關卡。讓我們來看看它到底是什麼東西,為什麼我們需要它。

#手寫裝飾器

現在我們要寫一個函式:

def add(x, y=10):    return x + y

然後我們想看看執行的結果,於是寫了幾個 print 語句:

print("add(10)",       add(10))print("add(20, 30)",   add(20, 30))print("add('a', 'b')", add('a', 'b'))# Results:# add(10) 20# add(20, 30) 50
# add('a', 'b') ab

現在我們想看看測試這個函式的效能,於是我們加上這個程式碼:

from time import timebefore = time()print("add(10)",       add(10))after = time()print("time taken: {}".format(after - before))before = time()print("add(20, 30)",   add(20, 30))after = time()print("time taken: {}".format(after - before))before = time()print("add('a', 'b')"
, add('a', 'b'))
after = time()print("time taken: {}".format(after - before))# Results# add(10) 20# time taken: 0.00017189979553222656# add(20, 30) 50# time taken: 9.751319885253906e-05# add('a', 'b') ab# time taken: 0.00012969970703125

程式碼馬上變得很複雜。但最重要的是,我們得寫一堆程式碼(複製貼上),程式設計師是懶惰的,所以我們就想到一些更簡單的方法,與其寫這麼多次,我們可以只寫一次程式碼:

from
time import time
def add(x, y=10): before = time() result = x + y after = time() print('elapsed: ', after - before) return resultprint("add(10)", add(10))print("add(20, 30)", add(20, 30))print("add('a', 'b')", add('a', 'b'))# Results# elapsed: 1.9073486328125e-06# add(10) 20# elapsed: 9.5367431640625e-07# add(20, 30) 50# elapsed: 1.9073486328125e-06# add('a', 'b') ab

不論是程式碼的修改量還是程式碼的美觀程度,都比之前的版本要好!

但是,現在我們寫了另一個函式:

def sub(x, y=10):    return x - y

我們必須再為 sub 函式加上和 add 相同的效能測試程式碼:

def sub(x, y=10):    before = time()    result = x - y    after = time()    print('elapsed: ', after - before)    return result

作為一個懶惰的程式設計師,我們立馬就發現了,有一個 “模式” 反覆出現,即執行一個函式,並計算這個函式的執行時間。於是我們就可以把這個模式抽象出來,用函式:

from time import timedef timer(func, x, y = 10):    before = time()    result = func(x, y)    after = time()    print("elapsed: ", after - before)    return resultdef add(x, y = 10):    return x + ydef sub(x, y = 10):    return x - yprint("add(10)", timer(add, 10))print("add(20, 30)", timer(add, 20, 30))

但這樣還是很麻煩,因為我們得改到所有的測試用例,把 add(20, 30) 改成 timer(add, 20, 30)。於是我們進一步改進,讓 timer 返回函式:

def timer(func):    def wraper(x, y=10):        before = time()        result = func(x, y)        after = time()        print("elapsed: ", after - before)        return result    return wraperdef add(x, y = 10):    return x + yadd = timer(add)def sub(x, y = 10):    return x - ysub = timer(sub)print("add(10)",       add(10))print("add(20, 30)",   add(20, 30))

這裡的最後一個問題是,我們的 timer 包裝的函式可能有不同的引數,於是我們可以進一步用 *args, **kwargs 來傳遞引數:

def timer(func):    def wraper(*args, **kwargs):        before = time()        result = func(*args, **kwargs)        after = time()        print("elapsed: ", after - before)        return result    return wraper

這裡的 timer 函式就是一個 “裝飾器”,它接受一個函式,並返回一個新的函式。在裝飾器的內部,對原函式進行了“包裝”。

#@ 語法糖

上一節是一個懶惰的程式設計師用原生的 Python 寫的裝飾器,但在裝飾器的使用上,用的是這個程式碼:

def add(x, y = 10):    return x + yadd = timer(add)        # <- notice thisdef sub(x, y = 10):    return x - ysub = timer(sub)

上面這個語句裡,我們把 add 的名字重複了 3 次,如果函式改了名字,我們就得改 3 處。懶惰的程式設計師就想了一個更“好”的方法,提供了一個語法來替換上面的內容:

@timerdef add(x, y=10):    return x + y

這就是我們最常見的裝飾器的形式了,這兩種寫法完全等價,只是 @ 寫法更簡潔一些。

#帶引數的裝飾器

我們知道下面兩種程式碼是等價的:

@decdef func(...):    ...func = dec(func)

我們可以把它當成是純文字的替換,於是可以是這樣的:

@dec(arg)def func(...):    ...func = dec(arg)(func)

這也就是我們看到的“帶引數”的裝飾器。可見,只要 dec(arg) 的返回值滿足 “裝飾器” 的定義即可。(接受一個函式,並返回一個新的函式)

這裡舉一個例子(來源):

def use_logging(level):    def decorator(func):        def wrapper(*args, **kwargs):            if level == "warn":                logging.warn("%s is running" % func.__name__)            elif level == "info":                logging.info("%s is running" % func.__name__)            return func(*args)        return wrapper    return decorator@use_logging(level="warn")def foo(name='foo'):    print("i am %s" % name)

先不管 use_logging 長什麼樣,先關心它的返回值 decorator,看到 decorator 本身是一個函式,並且引數是函式,返回值是函式,於是確認 decorator 是一個 “裝飾器”。於是上面這種“帶引數的裝飾器”的作用也就很直接了。

#類作為裝飾器

如果說 Python 裡一切都是物件的話,那函式怎麼表示成物件呢?其實只需要一個類實現 __call__ 方法即可。

class Timer:    def __init__(self, func):        self._func = func    def __call__(self, *args, **kwargs):        before = time()        result = self._func(*args, **kwargs)        after = time()        print("elapsed: ", after - before)        return result@Timerdef add(x, y=10):    return x + y

也就是說把類的建構函式當成了一個裝飾器,它接受一個函式作為引數,並返回了一個物件,而由於物件實現了 __call__ 方法,因此返回的物件相當於返回了一個函式。因此該類的建構函式就是一個裝飾器。

#小結

裝飾器中還有一些其它的話題,例如裝飾器中元資訊的丟失,如何在類及類的方法上使用裝飾器等。但本文裡我們主要目的是簡單介紹裝飾器的原因及一般的使用方法,能用上的地方就大膽地用上吧!

#擴充套件閱讀