來源:http://www.lightxue.com/under...python
Python有大量強大又貼心的特性,若是要列個最受歡迎排行榜,那麼裝飾器絕對會在其中。
剛接觸裝飾器,會以爲代碼很少卻難以理解。其實裝飾器的語法自己挺簡單的,複雜是由於同時混雜了其它的概念。下面咱們一塊兒拋去無關概念,簡單地理解下Python的裝飾器。閉包
在解釋器下跑個裝飾器的例子,直觀地感覺一下app
# make_bold就是裝飾器,實現方式這裏略去 >>> @make_bold ... def get_content(): ... return 'hello world' ... >>> get_content() '<b>hello world</b>'
被make_bold裝飾的get_content,調用後返回結果會自動被b標籤包住。怎麼作到的呢,簡單4步就能明白了。函數
咱們定義個get_content函數。這時get_content也是個對象,它能作全部對象的操做。設計
它有id,有type,有值。code
>>> id(get_content) 140090200473112 >>> type(get_content) <class 'function'> >>> get_content <function get_content at 0x7f694aa2be18>
跟其餘對象同樣能夠被賦值給其它變量。orm
>>> func_name = get_content >>> func_name() 'hello world'
它能夠當參數傳遞,也能夠當返回值對象
>>> def foo(bar): ... print(bar()) ... return bar ... >>> func = foo(get_content) hello world >>> func() 'hello world'
咱們能夠用class來構造函數對象。有成員函數__call__的就是函數對象了,函數對象被調用時正是調用的__call__。ip
class FuncObj(object): def __init__(self, name): print('Initialize') self.name= name def __call__(self): print('Hi', self.name)
咱們來調用看看。能夠看到,函數對象的使用分兩步:構造和調用(同窗們注意了,這是考點)。get
>>> fo = FuncObj('python') Initialize >>> fo() "Hi python"
裝飾器的@沒有作什麼特別的事,不用它也能夠實現同樣的功能,只不過須要更多的代碼。
@make_bold def get_content(): return 'hello world'
上面的代碼等價於下面的
def get_content(): return 'hello world' get_content = make_bold(get_content)
make_bold是個函數,要求入參是函數對象,返回值是函數對象。@的語法糖實際上是省去了上面最後一行代碼,使可讀性更好。用了裝飾器後,每次調用get_content,真正調用的是make_bold返回的函數對象。
入參是函數對象,返回是函數對象,若是第2步裏的類的構造函數改爲入參是個函數對象,不就正好符合要求嗎?咱們來試試實現make_bold。
class make_bold(object): def __init__(self, func): print('Initialize') self.func = func def __call__(self): print('Call') return '<b>{}</b>'.format(self.func())
大功告成,看看能不能用。
>>> @make_bold ... def get_content(): ... return 'hello world' ... Initialize >>> get_content() Call '<b>hello world</b>'
成功實現裝飾器!是否是很簡單?
這裏分析一下以前強調的構造和調用兩個過程。咱們去掉@語法糖好理解一些。
# 構造,使用裝飾器時構造函數對象,調用了__init__ >>> get_content = make_bold(get_content) Initialize # 調用,實際上直接調用的是make_bold構造出來的函數對象 >>> get_content() Call '<b>hello world</b>'
到這裏就完全清楚了,完結撒花,能夠關掉網頁了~~~(若是隻是想知道裝飾器原理的話)
閱讀源碼時,常常見到用嵌套函數實現的裝飾器,怎麼理解?一樣僅需4步。
用class實現的函數對象很容易看到何時構造的,那def定義的函數對象何時構造的呢?
# 這裏的全局變量刪去了無關的內容 >>> globals() {} >>> def func(): ... pass ... >>> globals() {'func': <function func at 0x10f5baf28>}
不像一些編譯型語言,程序在啓動時函數已經構造那好了。上面的例子能夠看到,執行到def會才構造出一個函數對象,並賦值給變量make_bold。
這段代碼和下面的代碼效果是很像的。
class NoName(object): def __call__(self): pass func = NoName()
Python的函數能夠嵌套定義。
def outer(): print('Before def:', locals()) def inner(): pass print('After def:', locals()) return inner #inner是在outer內定義的,因此算outer的局部變量。 #執行到def inner時函數對象才建立,所以每次調用outer都會建立一個新的inner。 #下面能夠看出,每次返回的inner是不一樣的。 >>> outer() Before def: {} After def: {'inner': <function outer.<locals>.inner at 0x7f0b18fa0048>} <function outer.<locals>.inner at 0x7f0b18fa0048> >>> outer() Before def: {} After def: {'inner': <function outer.<locals>.inner at 0x7f0b18fa00d0>} <function outer.<locals>.inner at 0x7f0b18fa00d0>
嵌套函數有什麼特別之處?由於有閉包。
def outer(): msg = 'hello world' def inner(): print(msg) return inner
下面的試驗代表,inner能夠訪問到outer的局部變量msg。
>>> func = outer() >>> func() hello world
閉包有2個特色
inner能訪問outer及其祖先函數的命名空間內的變量(局部變量,函數參數)。
調用outer已經返回了,可是它的命名空間被返回的inner對象引用,因此還不會被回收。
這部分想深刻能夠去了解Python的LEGB規則。
裝飾器要求入參是函數對象,返回值是函數對象,嵌套函數徹底能勝任。
def make_bold(func): print('Initialize') def wrapper(): print('Call') return '<b>{}</b>'.format(func()) return wrapper
用法跟類實現的裝飾器同樣。能夠去掉@語法糖分析下構造和調用的時機。
>>> @make_bold ... def get_content(): ... return 'hello world' ... Initialize >>> get_content() Call '<b>hello world</b>'
由於返回的wrapper還在引用着,因此存在於make_bold命名空間的func不會消失。make_bold能夠裝飾多個函數,wrapper不會調用混淆,由於每次調用make_bold,都會有建立新的命名空間和新的wrapper。
到此函數實現裝飾器也理清楚了,完結撒花,能夠關掉網頁了~~~(後面是使用裝飾的常見問題)
帶參數的裝飾器,有時會異常的好用。咱們看個例子。
>>> @make_header(2) ... def get_content(): ... return 'hello world' ... >>> get_content() '<h2>hello world</h2>' #怎麼作到的呢?其實這跟裝飾器語法沒什麼關係。去掉@語法糖會變得很容易理解。 @make_header(2) def get_content(): return 'hello world' # 等價於 def get_content(): return 'hello world' unnamed_decorator = make_header(2) get_content = unnamed_decorator(get_content)
上面代碼中的unnamed_decorator纔是真正的裝飾器,make_header是個普通的函數,它的返回值是裝飾器。
來看一下實現的代碼。
def make_header(level): print('Create decorator') # 這部分跟一般的裝飾器同樣,只是wrapper經過閉包訪問了變量level def decorator(func): print('Initialize') def wrapper(): print('Call') return '<h{0}>{1}</h{0}>'.format(level, func()) return wrapper # make_header返回裝飾器 return decorator
看了實現代碼,裝飾器的構造和調用的時序已經很清楚了。
>>> @make_header(2) ... def get_content(): ... return 'hello world' ... Create decorator Initialize >>> get_content() Call '<h2>hello world</h2>'
爲了有條理地理解裝飾器,以前例子裏的被裝飾函數有意設計成無參的。咱們來看個例子。
@make_bold def get_login_tip(name): return 'Welcome back, {}'.format(name)
最直接的想法是把get_login_tip的參數透傳下去。
class make_bold(object): def __init__(self, func): self.func = func def __call__(self, name): return '<b>{}</b>'.format(self.func(name))
若是被裝飾的函數參數是明確固定的,這麼寫是沒有問題的。可是make_bold明顯不是這種場景。它既須要裝飾沒有參數的get_content,又須要裝飾有參數的get_login_tip。這時候就須要可變參數了。
class make_bold(object): def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): return '<b>{}</b>'.format(self.func(*args, **kwargs))
當裝飾器不關心被裝飾函數的參數,或是被裝飾函數的參數多種多樣的時候,可變參數很是合適。可變參數不屬於裝飾器的語法內容,這裏就不深刻探討了。
下面這麼寫合法嗎?
@make_italic @make_bold def get_content(): return 'hello world'
合法。上面的的代碼和下面等價,留意一下裝飾的順序。
def get_content(): return 'hello world' get_content = make_bold(get_content) # 先裝飾離函數定義近的 get_content = make_italic(get_content)
Python的裝飾器倍感貼心的地方是對調用方透明。調用方徹底不知道也不須要知道調用的函數被裝飾了。這樣咱們就能在調用方的代碼徹底不改動的前提下,給函數patch功能。
爲了對調用方透明,裝飾器返回的對象要假裝成被裝飾的函數。假裝得越像,對調用方來講差別越小。有時光假裝函數名和參數是不夠的,由於Python的函數對象有一些元信息調用方可能讀取了。爲了連這些元信息也假裝上,functools.wraps出場了。它能用於把被調用函數的__module__,__name__,__qualname__,__doc__,__annotations__賦值給裝飾器返回的函數對象。
import functools def make_bold(func): @functools.wraps(func) def wrapper(*args, **kwargs): return '<b>{}</b>'.format(func(*args, **kwargs)) return wrapper
對比一下效果。
>>> @make_bold ... def get_content(): ... '''Return page content''' ... return 'hello world' >>> # 不用functools.wraps的結果 >>> get_content.__name__ 'wrapper' >>> get_content.__doc__ >>> # 用functools.wraps的結果 >>> get_content.__name__ 'get_content' >>> get_content.__doc__ 'Return page content'
實現裝飾器時每每不知道調用方會怎麼用,因此養成好習慣加上functools.wraps吧。