Python有大量強大又貼心的特性,如果要列個最受歡迎排行榜,那麼裝飾器絕對會在其中。
剛接觸裝飾器,會覺得程式碼不多卻難以理解。其實裝飾器的語法本身挺簡單的,複雜是因為同時混雜了其它的概念。下面我們一起拋去無關概念,簡單地理解下Python的裝飾器。
裝飾器的原理
在直譯器下跑個裝飾器的例子,直觀地感受一下。
1 2 3 4 5 6 7 |
# make_bold就是裝飾器,實現方式這裡略去 >>> @make_bold ... def get_content(): ... return 'hello world' ... >>> get_content() '<b>hello world</b>' |
被make_bold裝飾的get_content,呼叫後返回結果會自動被b標籤包住。怎麼做到的呢,簡單4步就能明白了。
1. 函式是物件
我們定義個get_content函式。這時get_content也是個物件,它能做所有物件的操作。
1 2 |
def get_content(): return 'hello world' |
它有id,有type,有值。
1 2 3 4 5 6 |
>>> id(get_content) 140090200473112 >>> type(get_content) <class 'function'> >>> get_content <function get_content at 0x7f694aa2be18> |
跟其他物件一樣可以被賦值給其它變數。
1 2 3 |
>>> func_name = get_content >>> func_name() 'hello world' |
它可以當引數傳遞,也可以當返回值
1 2 3 4 5 6 7 8 |
>>> def foo(bar): ... print(bar()) ... return bar ... >>> func = foo(get_content) hello world >>> func() 'hello world' |
2. 自定義函式物件
我們可以用class來建構函式物件。有成員函式__call__的就是函式物件了,函式物件被呼叫時正是呼叫的__call__。
1 2 3 4 5 6 7 |
class FuncObj(object): def __init__(self, name): print('Initialize') self.name= name def __call__(self): print('Hi', self.name) |
我們來呼叫看看。可以看到,函式物件的使用分兩步:構造和呼叫(同學們注意了,這是考點)。
1 2 3 4 |
>>> fo = FuncObj('python') Initialize >>> fo() Hi python |
3. @是個語法糖
裝飾器的@沒有做什麼特別的事,不用它也可以實現一樣的功能,只不過需要更多的程式碼。
1 2 3 4 5 6 7 8 9 |
@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返回的函式物件。
4. 用類實現裝飾器
入參是函式物件,返回是函式物件,如果第2步裡的類的建構函式改成入參是個函式物件,不就正好符合要求嗎?我們來試試實現make_bold。
1 2 3 4 5 6 7 8 |
class make_bold(object): def __init__(self, func): print('Initialize') self.func = func def __call__(self): print('Call') return '<b>{}</b>'.format(self.func()) |
大功告成,看看能不能用。
1 2 3 4 5 6 7 8 |
>>> @make_bold ... def get_content(): ... return 'hello world' ... Initialize >>> get_content() Call '<b>hello world</b>' |
成功實現裝飾器!是不是很簡單?
這裡分析一下之前強調的構造和呼叫兩個過程。我們去掉@語法糖好理解一些。
1 2 3 4 5 6 7 8 |
# 構造,使用裝飾器時建構函式物件,呼叫了__init__ >>> get_content = make_bold(get_content) Initialize # 呼叫,實際上直接呼叫的是make_bold構造出來的函式物件 >>> get_content() Call '<b>hello world</b>' |
到這裡就徹底清楚了,完結撒花,可以關掉網頁了~~~(如果只是想知道裝飾器原理的話)
函式版裝飾器
閱讀原始碼時,經常見到用巢狀函式實現的裝飾器,怎麼理解?同樣僅需4步。
1. def的函式物件初始化
用class實現的函式物件很容易看到什麼時候構造的,那def定義的函式物件什麼時候構造的呢?
1 2 3 4 5 6 7 8 |
# 這裡的全域性變數刪去了無關的內容 >>> globals() {} >>> def func(): ... pass ... >>> globals() {'func': <function func at 0x10f5baf28>} |
不像一些編譯型語言,程式在啟動時函式已經構造那好了。上面的例子可以看到,執行到def會才構造出一個函式物件,並賦值給變數make_bold。
這段程式碼和下面的程式碼效果是很像的。
1 2 3 4 5 |
class NoName(object): def __call__(self): pass func = NoName() |
2. 巢狀函式
Python的函式可以巢狀定義。
1 2 3 4 5 6 |
def outer(): print('Before def:', locals()) def inner(): pass print('After def:', locals()) return inner |
inner是在outer內定義的,所以算outer的區域性變數。執行到def inner時函式物件才建立,因此每次呼叫outer都會建立一個新的inner。下面可以看出,每次返回的inner是不同的。
1 2 3 4 5 6 7 8 |
>>> 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> |
3. 閉包
巢狀函式有什麼特別之處?因為有閉包。
1 2 3 4 5 |
def outer(): msg = 'hello world' def inner(): print(msg) return inner |
下面的試驗表明,inner可以訪問到outer的區域性變數msg。
1 2 3 |
>>> func = outer() >>> func() hello world |
閉包有2個特點
- inner能訪問outer及其祖先函式的名稱空間內的變數(區域性變數,函式引數)。
- 呼叫outer已經返回了,但是它的名稱空間被返回的inner物件引用,所以還不會被回收。
這部分想深入可以去了解Python的LEGB規則。
4. 用函式實現裝飾器
裝飾器要求入參是函式物件,返回值是函式物件,巢狀函式完全能勝任。
1 2 3 4 5 6 |
def make_bold(func): print('Initialize') def wrapper(): print('Call') return '<b>{}</b>'.format(func()) return wrapper |
用法跟類實現的裝飾器一樣。可以去掉@語法糖分析下構造和呼叫的時機。
1 2 3 4 5 6 7 8 |
>>> @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。
到此函式實現裝飾器也理清楚了,完結撒花,可以關掉網頁了~~~(後面是使用裝飾的常見問題)
常見問題
1. 怎麼實現帶引數的裝飾器?
帶引數的裝飾器,有時會異常的好用。我們看個例子。
1 2 3 4 5 6 |
>>> @make_header(2) ... def get_content(): ... return 'hello world' ... >>> get_content() '<h2>hello world</h2>' |
怎麼做到的呢?其實這跟裝飾器語法沒什麼關係。去掉@語法糖會變得很容易理解。
1 2 3 4 5 6 7 8 9 10 |
@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是個普通的函式,它的返回值是裝飾器。
來看一下實現的程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 |
看了實現程式碼,裝飾器的構造和呼叫的時序已經很清楚了。
1 2 3 4 5 6 7 8 9 |
>>> @make_header(2) ... def get_content(): ... return 'hello world' ... Create decorator Initialize >>> get_content() Call '<h2>hello world</h2>' |
2. 如何裝飾有引數的函式?
為了有條理地理解裝飾器,之前例子裡的被裝飾函式有意設計成無參的。我們來看個例子。
1 2 3 |
@make_bold def get_login_tip(name): return 'Welcome back, {}'.format(name) |
最直接的想法是把get_login_tip的引數透傳下去。
1 2 3 4 5 6 |
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。這時候就需要可變引數了。
1 2 3 4 5 |
class make_bold(object): def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): return '<b>{}</b>'.format(self.func(*args, **kwargs)) |
當裝飾器不關心被裝飾函式的引數,或是被裝飾函式的引數多種多樣的時候,可變引數非常合適。可變引數不屬於裝飾器的語法內容,這裡就不深入探討了。
3. 一個函式能否被多個裝飾器裝飾?
下面這麼寫合法嗎?
1 2 3 4 |
@make_italic @make_bold def get_content(): return 'hello world' |
合法。上面的的程式碼和下面等價,留意一下裝飾的順序。
1 2 3 4 |
def get_content(): return 'hello world' get_content = make_bold(get_content) # 先裝飾離函式定義近的 get_content = make_italic(get_content) |
4. functools.wraps有什麼用?
Python的裝飾器倍感貼心的地方是對呼叫方透明。呼叫方完全不知道也不需要知道呼叫的函式被裝飾了。這樣我們就能在呼叫方的程式碼完全不改動的前提下,給函式patch功能。
為了對呼叫方透明,裝飾器返回的物件要偽裝成被裝飾的函式。偽裝得越像,對呼叫方來說差異越小。有時光偽裝函式名和引數是不夠的,因為Python的函式物件有一些元資訊呼叫方可能讀取了。為了連這些元資訊也偽裝上,functools.wraps出場了。它能用於把被呼叫函式的__module__,__name__,__qualname__,__doc__,__annotations__賦值給裝飾器返回的函式物件。
1 2 3 4 5 6 7 |
import functools def make_bold(func): @functools.wraps(func) def wrapper(*args, **kwargs): return '<b>{}</b>'.format(func(*args, **kwargs)) return wrapper |
對比一下效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
>>> @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吧。
這次是真•完結了,有疑問請留言,撒花吧~~~