看完這篇文章還不理解裝飾器,說明我寫的還不夠清晰,鼓勵鼓勵我吧。python
講 Python 裝飾器前,我想先舉個例子,雖有點污,但跟裝飾器這個話題很貼切。程序員
每一個人都有的內褲主要功能是用來遮羞,可是到了冬天它無法爲咱們防風禦寒,咋辦?咱們想到的一個辦法就是把內褲改造一下,讓它變得更厚更長,這樣一來,它不只有遮羞功能,還能提供保暖,不過有個問題,這個內褲被咱們改形成了長褲後,雖然還有遮羞功能,但本質上它再也不是一條真正的內褲了。因而聰明的人們發明長褲,在不影響內褲的前提下,直接把長褲套在了內褲外面,這樣內褲仍是內褲,有了長褲後寶寶不再冷了。裝飾器就像咱們這裏說的長褲,在不影響內褲做用的前提下,給咱們的身子提供了保暖的功效。編程
談裝飾器前,還要先要明白一件事,Python 中的函數和 Java、C++不太同樣,Python 中的函數能夠像普通變量同樣當作參數傳遞給另一個函數,例如: 數組
def foo():
print("foo")
def bar(func):
func()
bar(foo)複製代碼
正式回到咱們的主題。裝飾器本質上是一個 Python 函數或類,它可讓其餘函數或類在不須要作任何代碼修改的前提下增長額外功能,裝飾器的返回值也是一個函數/類對象。它常常用於有切面需求的場景,好比:插入日誌、性能測試、事務處理、緩存、權限校驗等場景,裝飾器是解決這類問題的絕佳設計。有了裝飾器,咱們就能夠抽離出大量與函數功能自己無關的雷同代碼到裝飾器中並繼續重用。歸納的講,裝飾器的做用就是爲已經存在的對象添加額外的功能。緩存
先來看一個簡單例子,雖然實際代碼可能比這複雜不少:閉包
def foo():
print('i am foo')複製代碼
如今有一個新的需求,但願能夠記錄下函數的執行日誌,因而在代碼中添加日誌代碼: app
def foo():
print('i am foo')
logging.info("foo is running")複製代碼
若是函數 bar()、bar2() 也有相似的需求,怎麼作?再寫一個 logging 在 bar 函數裏?這樣就形成大量雷同的代碼,爲了減小重複寫代碼,咱們能夠這樣作,從新定義一個新的函數:專門處理日誌 ,日誌處理完以後再執行真正的業務代碼 函數
def use_logging(func):
logging.warn("%s is running" % func.__name__)
func()
def foo():
print('i am foo')
use_logging(foo)複製代碼
這樣作邏輯上是沒問題的,功能是實現了,可是咱們調用的時候再也不是調用真正的業務邏輯 foo 函數,而是換成了 use_logging 函數,這就破壞了原有的代碼結構, 如今咱們不得不每次都要把原來的那個 foo 函數做爲參數傳遞給 use_logging 函數,那麼有沒有更好的方式的呢?固然有,答案就是裝飾器。性能
def use_logging(func):
def wrapper():
logging.warn("%s is running" % func.__name__)
return func() # 把 foo 當作參數傳遞進來時,執行func()就至關於執行foo()
return wrapper
def foo():
print('i am foo')
foo = use_logging(foo) # 由於裝飾器 use_logging(foo) 返回的時函數對象 wrapper,這條語句至關於 foo = wrapper
foo() # 執行foo()就至關於執行 wrapper()複製代碼
use_logging 就是一個裝飾器,它一個普通的函數,它把執行真正業務邏輯的函數 func 包裹在其中,看起來像 foo 被 use_logging 裝飾了同樣,use_logging 返回的也是一個函數,這個函數的名字叫 wrapper。在這個例子中,函數進入和退出時 ,被稱爲一個橫切面,這種編程方式被稱爲面向切面的編程。測試
若是你接觸 Python 有一段時間了的話,想必你對 @ 符號必定不陌生了,沒錯 @ 符號就是裝飾器的語法糖,它放在函數開始定義的地方,這樣就能夠省略最後一步再次賦值的操做。
def use_logging(func):
def wrapper():
logging.warn("%s is running" % func.__name__)
return func()
return wrapper
@use_logging
def foo():
print("i am foo")
foo()複製代碼
如上所示,有了 @ ,咱們就能夠省去foo = use_logging(foo)
這一句了,直接調用 foo() 便可獲得想要的結果。大家看到了沒有,foo() 函數不須要作任何修改,只需在定義的地方加上裝飾器,調用的時候仍是和之前同樣,若是咱們有其餘的相似函數,咱們能夠繼續調用裝飾器來修飾函數,而不用重複修改函數或者增長新的封裝。這樣,咱們就提升了程序的可重複利用性,並增長了程序的可讀性。
裝飾器在 Python 使用如此方便都要歸因於 Python 的函數能像普通的對象同樣能做爲參數傳遞給其餘函數,能夠被賦值給其餘變量,能夠做爲返回值,能夠被定義在另一個函數內。
可能有人問,若是個人業務邏輯函數 foo 須要參數怎麼辦?好比:
def foo(name):
print("i am %s" % name)複製代碼
咱們能夠在定義 wrapper 函數的時候指定參數:
def wrapper(name):
logging.warn("%s is running" % func.__name__)
return func(name)
return wrapper複製代碼
這樣 foo 函數定義的參數就能夠定義在 wrapper 函數中。這時,又有人要問了,若是 foo 函數接收兩個參數呢?三個參數呢?更有甚者,我可能傳不少個。當裝飾器不知道 foo 到底有多少個參數時,咱們能夠用 *args 來代替:
def wrapper(*args):
logging.warn("%s is running" % func.__name__)
return func(*args)
return wrapper複製代碼
如此一來,甭管 foo 定義了多少個參數,我均可以完整地傳遞到 func 中去。這樣就不影響 foo 的業務邏輯了。這時還有讀者會問,若是 foo 函數還定義了一些關鍵字參數呢?好比:
def foo(name, age=None, height=None):
print("I am %s, age %s, height %s" % (name, age, height))複製代碼
這時,你就能夠把 wrapper 函數指定關鍵字函數:
def wrapper(*args, **kwargs):
# args是一個數組,kwargs一個字典
logging.warn("%s is running" % func.__name__)
return func(*args, **kwargs)
return wrapper複製代碼
裝飾器還有更大的靈活性,例如帶參數的裝飾器,在上面的裝飾器調用中,該裝飾器接收惟一的參數就是執行業務的函數 foo 。裝飾器的語法容許咱們在調用時,提供其它參數,好比@decorator(a)
。這樣,就爲裝飾器的編寫和使用提供了更大的靈活性。好比,咱們能夠在裝飾器中指定日誌的等級,由於不一樣業務函數可能須要的日誌級別是不同的。
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)
foo()複製代碼
上面的 use_logging 是容許帶參數的裝飾器。它其實是對原有裝飾器的一個函數封裝,並返回一個裝飾器。咱們能夠將它理解爲一個含有參數的閉包。當我
們使用@use_logging(level="warn")
調用的時候,Python 可以發現這一層的封裝,並把參數傳遞到裝飾器的環境中。
@use_logging(level="warn")
等價於@decorator
沒錯,裝飾器不只能夠是函數,還能夠是類,相比函數裝飾器,類裝飾器具備靈活度大、高內聚、封裝性等優勢。使用類裝飾器主要依靠類的__call__
方法,當使用 @ 形式將裝飾器附加到函數上時,就會調用此方法。
class Foo(object):
def __init__(self, func):
self._func = func
def __call__(self):
print ('class decorator runing')
self._func()
print ('class decorator ending')
@Foo
def bar():
print ('bar')
bar()複製代碼
使用裝飾器極大地複用了代碼,可是他有一個缺點就是原函數的元信息不見了,好比函數的docstring
、__name__
、參數列表,先看例子:
# 裝飾器
def logged(func):
def with_logging(*args, **kwargs):
print func.__name__ # 輸出 'with_logging'
print func.__doc__ # 輸出 None
return func(*args, **kwargs)
return with_logging
# 函數
@logged
def f(x):
"""does some math"""
return x + x * x
logged(f)複製代碼
不難發現,函數 f 被with_logging
取代了,固然它的docstring
,__name__
就是變成了with_logging
函數的信息了。好在咱們有functools.wraps
,wraps
自己也是一個裝飾器,它能把原函數的元信息拷貝到裝飾器裏面的 func 函數中,這使得裝飾器裏面的 func 函數也有和原函數 foo 同樣的元信息了。
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print func.__name__ # 輸出 'f'
print func.__doc__ # 輸出 'does some math'
return func(*args, **kwargs)
return with_logging
@logged
def f(x):
"""does some math"""
return x + x * x複製代碼
一個函數還能夠同時定義多個裝飾器,好比:
@a
@b
@c
def f ():
pass複製代碼
它的執行順序是從裏到外,最早調用最裏層的裝飾器,最後調用最外層的裝飾器,它等效於
f = a(b(c(f)))複製代碼
關注公衆號『一個程序員的微站』 分享Python乾貨和有溫度的內容