之前有比較系統介紹過Python的裝飾器(請查閱《詳解Python裝飾器》),本文算是一個補充。今天我們一起探討一下裝飾器的另類用法。
語法回顧
開始之前我們再將Python裝飾器的語法回顧一下。
1 2 3 |
@ decorate def f(...): pass |
等同於:
1 2 3 4 |
def f(...): pass f = decorate(f) |
@語法的好處在於:
- 相同的函式名只出現一次,避免了
f = decorate(f)
這樣的語句。 - 可讀性更高,讓讀程式碼的人一眼就明白函式被裝飾了哪些功能。
@call()裝飾器
假設你要建立一個整數平方的列表,你可以這樣寫:
1 2 3 |
>>> table = [0, 1, 4, 9, 16] >>> len(table), table[3] (5, 9) |
也可以使用列表表示式,因為我們要實現比較簡單。
1 2 3 |
>>> table = [i * i for i in range(5)] >>> len(table), table[3] (5, 9) |
但是假如這個列表的邏輯比較複雜的時候,最好是寫成一個方法,這樣會更好維護。
1 2 3 4 5 6 |
>>> def table(n): ... value = [] ... for i in range(n): ... value.append(i*i) ... return value >>> table = table(5) |
注意看最後一句,是不是很符合裝飾器的語法規則?什麼情況下你會寫這樣的程式碼呢?
- 你需要把相對複雜業務寫成一個方法。
- 這個方法和返回值可以同名,而且你不希望對外公開此方法,只公開結果。
- 你想盡量使用裝飾器。(無厘頭的理由)
那麼這時候@call()
裝飾器就登場了。
1 2 3 4 |
def call(*args, **kwargs): def call_fn(fn): return fn(*args, **kwargs) return call_fn |
這個裝飾器會把你傳入的引數送給目標函式然後直接執行。
1 2 3 4 5 6 7 8 |
@call(5) def table(n): value = [] for i in range(n): value.append(i*i) return value print len(table), table[3] # 5 9 |
@call()
裝飾器適用於任何函式,你傳入的引數會被直接使用然後結果賦值給同名函式。這樣避免了你重新定義一個變數來儲存結果。
@list 裝飾器
假如你有一個這樣一個生成器函式。
1 2 3 |
def table(n): for i in range(n): yield i |
當你要生成n=5
的序列時,可以直接呼叫。
1 2 |
table = table(5) print table # <generator object table at 0x027DAC10> |
使用上節提到的@call()
裝飾器,也能得到一樣的結果。
1 2 3 4 5 6 |
@call(5) def table(n): for i in range(n): yield i print table # <generator object table at 0x0340AC10> |
你還可以直接將其轉換成列表。(使用list(generator_object)
函式)
1 2 3 4 5 6 7 |
<a href="http://www.jobbole.com/members/wx1825862276">@list</a> @call(5) def table(n): for i in range(n): yield i print table # [0, 1, 2, 3, 4] |
相信不少同學第一次看到這個用法應該是懵逼的。這等同於列表表示式,但是可讀性也許差了不少。例子本身只是演示了裝飾器的一種用法,但不是推薦你就這樣使用裝飾器。你這樣用也許會被其他同事拖到牆角里打死。
類裝飾器
在Python 2.6以前,還不支援類裝飾器。也就是說,你不能使用這樣的寫法。
1 2 3 |
@ decorator class MyClass(object): pass |
你必須這樣寫:
1 2 3 4 |
class MyClass(object): pass MyClass = decorator(MyClass) |
也就是說,@語法對類是做了特殊處理的,類不一定是一個callable物件(儘管它有建構函式),但是也允許使用裝飾器。那麼基於以上語法,你覺得類裝飾器能實現什麼功能呢?
舉一個例子,ptest中的@TestClass()
用於宣告一個測試類,其原始碼大致如此。
1 2 3 4 5 6 7 |
def TestClass(enabled=True, run_mode="singleline"): def tracer(cls): cls.__pd_type__ ='test' cls.__enabled__ = enabled cls.__run_mode__ = run_mode.lower() return cls return tracer |
當我們在寫一個測試類時,發生了什麼?
1 2 3 4 5 |
@TestClass() class TestCases(object): # your test case ... print TestCases.__dict__ # {'__module__': '__main__', '__enabled__': True, '__pd_type__': 'test', '__run_mode__': 'singleline', ...} |
居然裝飾器的引數全都變成了變成這個類的屬性,好神奇!我們把語法糖一一展開。
1 2 3 4 5 6 7 8 9 10 |
class TestCases(object): pass decorator = TestClass() print decorator # <function tracer at 0x033128F0> TestCases = decorator(TestCases) print TestCases # <class '__main__.TestCases'> print TestCases.__dict__ # {'__module__': '__main__', '__enabled__': True, '__pd_type__': 'test', '__run_mode__': 'singleline', ...} |
當裝飾器在被使用時,TestClass()
函式會馬上被執行並返回一個裝飾器函式,這個函式是一個閉包函式,儲存了enabled
和run_mode
兩個變數。另外它還接受一個類作為引數,並使用之前儲存的變數為這個類新增屬性,最後返回。所以經過@TestClass()
裝飾過的類都會帶上__enabled__
、__pd_type__
以及__run_mode__
的屬性。
由此可見,類裝飾器可以完成和Java類似的註解功能,而且要比註解強大的多。
後記
裝飾器就是一個語法糖,當你看不懂一個裝飾器時,可以考慮將其依次展開,分別帶入。這個語法糖給了我們不少方便,但是也要慎用。畢竟可維護的程式碼才是高質量的程式碼。