Python Decorator(裝飾器)

RicardoJack 7年前發布 | 35K 次閱讀 Python Python開發

今天來說說 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 time

before = 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 result

print("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 time

def timer(func, x, y = 10):
 before = time()
 result = func(x, y)
 after = time()
 print("elapsed: ", after - before)
 return result

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

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

print("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 wraper

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

def sub(x, y = 10):
 return x - y
sub = 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 函數就是一個 “裝飾器”,它接受一個函數,并返回一個新的函數。在裝飾器的內部,對原函數進行了“包裝”。

注:上面的例子取自 What Does it Take to Be an Expert At Python

@ 語法糖

上一節是一個懶惰的程序員用原生的 Python 寫的裝飾器,但在裝飾器的使用上,用的是這個代碼:

def add(x, y = 10):
 return x + y
add = timer(add) # <- notice this

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

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

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

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

帶參數的裝飾器

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

@dec
def 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

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

也就是說把類的構造函數當成了一個裝飾器,它接受一個函數作為參數,并返回了一個對象,而由于對象實現了 __call__ 方法,因此返回的對象相當于返回了一個函數。因此該類的構造函數就是一個裝飾器。

小結

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

擴展閱讀

 

來自:http://lotabout.me/2017/Python-Decorator/

 

 本文由用戶 RicardoJack 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!