python 装饰器 继承_Python设计模式之装饰器模式
裝飾器模式
無論何時我們想對一個對象添加額外的功能,都有下面這些不同的可選方法。
如果合理,可以直接將功能添加到對象所屬的類(例如,添加一個新的方法)
使用組合
使用繼承
注意,本文中的Decorator可以為裝飾器或者修飾器。
與繼承相比,通常應該優先選擇組合,因為繼承使得代碼更難復用,繼承關系是靜態的,并且應用于整個類以及這個類的所有實例(請參考[GOF95,第31頁]和網頁[t.cn/RqrC8Yo])。
設計模式為我們提供第四種可選方法,以支持動態地(運行時)擴展一個對象的功能,這種方法就是修飾器。修飾器(Decorator)模式能夠以透明的方式(不會影響其他對象)動態地將功能添加到一個對象中(請參考[GOF95,第196頁])。
在許多編程語言中,使用子類化(繼承)來實現修飾器模式(請參考[GOF95,第198頁])。在Python中,我們可以(并且應該)使用內置的修飾器特性。一個Python修飾器就是對Python語法的一個特定改變,用于擴展一個類、方法或函數的行為,而無需使用繼承。從實現的角度來說,Python修飾器是一個可調用對象(函數、方法、類),接受一個函數對象fin作為輸入,并返回另一個函數對象fout(請參考網頁)。這意味著可以將任何具有這些屬性的可調用對象當作一個修飾器。在第1章和第2章中已經看到如何使用內置的property修飾器讓一個方法表現為一個變量。在5.4節,我們將學習如何實現及使用我們自己的修飾器。
修飾器模式和Python修飾器之間并不是一對一的等價關系。Python修飾器能做的實際上比修飾器模式多得多,其中之一就是實現修飾器模式(請參考[Eckel08,第59頁]和網頁[t.cn/RqrlLcQ])。
#!/usr/bin/env python
"""https://docs.python.org/2/library/functools.html#functools.wraps"""
"""https://stackoverflow.com/questions/739654/how-can-i-make-a-chain-of-function-decorators-in-python/739665#739665"""
from functools import wraps
def makebold(fn):
return getwrapped(fn, "b")
def makeitalic(fn):
return getwrapped(fn, "i")
def getwrapped(fn, tag):
@wraps(fn)
def wrapped():
return "%s%s>" % (tag, fn(), tag)
return wrapped
@makebold
@makeitalic
def hello():
"""a decorated hello world"""
return "hello world"
if __name__ == '__main__':
print('result:{} name:{} doc:{}'.format(hello(), hello.__name__, hello.__doc__))
### OUTPUT ###
# result:hello world name:hello doc:a decorated hello world
result:hello world name:hello doc:a decorated hello world
# http://stackoverflow.com/questions/3118929/implementing-the-decorator-pattern-in-python
class foo(object):
def f1(self):
print("original f1")
def f2(self):
print("original f2")
class foo_decorator(object):
def __init__(self, decoratee):
self._decoratee = decoratee
def f1(self):
print("decorated f1")
self._decoratee.f1()
def __getattr__(self, name):
return getattr(self._decoratee, name) # 這個不是delegation么
u = foo()
v = foo_decorator(u)
v.f1()
v.f2()
decorated f1
original f1
original f2
現實中的例子
該模式雖名為修飾器,但這并不意味著它應該只用于讓產品看起來更漂亮。修飾器模式通常用于擴展一個對象的功能。這類擴展的實際例子有,給槍加一個消音器、使用不同的照相機鏡頭(在可拆卸鏡頭的照相機上)等。
下圖由sourcemaking.com提供,展示了我們可以如何使用一些專用配件來修飾一把槍,使其 無聲、更準以及更具破壞力(請參考網頁[t.cn/RqrC8Yo])。注意,圖中使用了子類化,但是在 Python中,這并不是必需的,因為可以使用語言內置的修飾器特性。
軟件中的例子
Django框架大量地使用修飾器,其中一個例子是視圖修飾器。Django的視圖(View)修飾器 可用于以下幾種用途(請參考網頁[t.cn/RqrlJbA])。
限制某些HTTP請求對視圖的訪問控制特定視圖上的緩存行為
按單個視圖控制壓縮
基于特定HTTP請求頭控制緩存
Grok框架也使用修飾器來實現不同的目標,比如下面幾種情況。
將一個函數注冊為事件訂閱者
以特定權限保護一個方法
實現適配器模式
應用案例
當用于實現橫切關注點(cross-cutting concerns)時,修飾器模式會大顯神威(請參考[Lott14,第223頁]和網頁[t.cn/Rqrl6O0])。以下是橫切關注點的一些例子。
數據校驗
事務處理(這里的事務類似于數據庫事務,意味著要么所有步驟都成功完成,要么事務失敗) ?緩存
日志
監控
調試
業務規則
壓縮
加密
一般來說,應用中有些部件是通用的,可應用于其他部件,這樣的部件被看作橫切關注點。
使用修飾器模式的另一個常見例子是圖形用戶界面(Graphical User Interface,GUI)工具集。在一個GUI工具集中,我們希望能夠將一些特性,比如邊框、陰影、顏色以及滾屏,添加到單個組件/部件。
實現
Python修飾器通用并且非常強大。你可以在Python官網python.org的修飾器代碼庫頁面(請參考網頁[t.cn/zRHPIq4])中找到許多修飾器的使用樣例。本節中,我們將學習如何實現一個memoization修飾器(請參考網頁[t.cn/zQi9AET])。所有遞歸函數都能因memoization而提速,那么來試試常用的斐波那契數列例子。使用遞歸算法實現斐波那契數列,直接了當,但性能問題較大,即使對于很小的數值也是如此。首先來看看樸素的實現方法(文件fibonacci_naive.py)。
def fibonacci(n):
assert(n >= 0), 'n must be >= 0'
return n if n in (0, 1) else fibonacci(n-1) + fibonacci(n-2)
if __name__ == '__main__':
from timeit import Timer
t = Timer('fibonacci(8)', 'from __main__ import fibonacci')
print(t.timeit())
15.40320448600687
執行一下這個例子就知道這種實現的速度有多慢了。計算第8個斐波那契數要花費運行的樣例輸出如上所示。
使用memoization方法看看能否改善。在下面的代碼中,我們使用一個dict來緩存斐波那契 數列中已經計算好的數值,同時也修改傳給fabonacci()函數的參數,計算第100個斐波那契數, 而不是第8個。
known = {0:0, 1:1}
def fibonacci(n):
assert(n >= 0), 'n must be >= 0'
if n in known:
return known[n]
res = fibonacci(n-1) + fibonacci(n-2)
known[n] = res
return res
if __name__ == '__main__':
from timeit import Timer
t = Timer('fibonacci(100)', 'from __main__ import fibonacci')
print(t.timeit())
0.30129148002015427
執行基于memoization的代碼實現,可以看到性能得到了極大的提升,甚至對于計算大的數 值性能也是可接受的。運行的樣例輸出如上所示。
但這種方法有一些問題。雖然性能不再是一個問題,但代碼也沒有不使用memoization時那 樣簡潔。如果我們決定擴展代碼,加入更多的數學函數,并將其轉變成一個模塊,那又會是什么 樣的呢?假設決定加入的下一個函數是nsum(),該函數返回前n個數字的和。注意這個函數已存 在于math模塊中,名為fsum(),但我們也能很容易就能想到標準庫中還沒有、但是對我們模塊 有用的其他函數(例如,帕斯卡三角形、埃拉托斯特尼篩法等)。所以暫且不必在意示例函數是 否已存在。使用memoization實現nsum()函數的代碼如下所示。
known_sum = {0:0}
def nsum(n):
assert(n >= 0), 'n must be >= 0'
if n in known_sum:
return known_sum[n]
res = n + nsum(n-1)
known_sum[n] = res
return res
你有沒有注意到其中的問題?多了一個名為known_sum的新字典,為nsum提供緩存作用, 并且函數本身也比不使用memoization時的更復雜。這個模塊逐步變得不必要地復雜。保持遞歸 函數與樸素版本的一樣簡單,但在性能上又能與使用memoization的函數相近,這可能嗎?幸運 的是,確實可能,解決方案就是使用修飾器模式。
首先創建一個如下面的例子所示的memoize()函數。這個修飾器接受一個需要使用 memoization的函數fn作為輸入,使用一個名為known的dict作為緩存。函數functools.wraps() 是一個為創建修飾器提供便利的函數;雖不強制,但推薦使用,因為它能保留被修飾函數的文檔字符串和簽名(請參考網頁[t.cn/Rqrl0K5])。這種情況要求參數列表args,因為被修飾的函數可能有輸入參數。如果fibonacci()和nsum()不需要任何參數,那么使用args確實是多余的,但它 們是需要參數n的。
from functools import wraps
def memoize(fn):
known = dict()
@wraps(fn)
def memoizer(*args):
if args not in known:
known[args] = fn(*args)
return known[args]
return memoizer
現在,對樸素版本的函數應用memoize()修飾器。這樣既能保持代碼的可讀性又不影響性能。 我們通過修飾(或修飾行)來應用一個修飾器。修飾使用@name語法,其中name是指我們想要使 用的修飾器的名稱。這其實只不過是一個簡化修飾器使用的語法糖。我們甚至可以繞過這個語法 手動執行修飾器,留給你作為練習吧。來看看下面的例子中如何對我們的遞歸函數使用memoize() 修飾器。
@memoize
def nsum(n):
'''返回前n個數字的和'''
assert(n >= 0), 'n must be <= 0'
return 0 if n == 0 else n + nsum(n-1)
@memoize
def fibonacci(n):
'''返回斐波那契數列的第n個數'''
assert(n >= 0), 'n must be >= 0'
return n if n in (0, 1) else fibonacci(n-1) + fibonacci(n-2)
代碼的最后一部分展示如何使用被修飾的函數,并測量其性能。measure是一個字典列表,用于避免代碼重復。注意name和doc分別是如何展示正確的函數名稱和文檔字符串值的。嘗試從memoize()中刪除@functools.wraps(fn)修飾,看看是否仍舊如此。
if __name__ == '__main__':
from timeit import Timer
measure = [ {'exec':'fibonacci(100)', 'import':'fibonacci', 'func':fibonacci},{'exec':'nsum(200)', 'import':'nsum', 'func':nsum} ]
for m in measure:
t = Timer('{}'.format(m['exec']), 'from __main__ import {}'.format(m['import']))
print('name: {}, doc: {}, executing: {}, time: {}'.format(m['func'].__name__, m['func'].__doc__, m['exec'], t.timeit()))
name: fibonacci, doc: 返回斐波那契數列的第n個數, executing: fibonacci(100), time: 0.29140055197058246
name: nsum, doc: 返回前n個數字的和, executing: nsum(200), time: 0.3004333569551818
看看我們數學模塊的完整代碼(文件mymath.py)和執行時的樣例輸出。
from functools import wraps
def memoize(fn):
known = dict()
@wraps(fn)
def memoizer(*args):
if args not in known:
known[args] = fn(*args)
return known[args]
return memoizer
@memoize
def nsum(n):
'''返回前n個數字的和'''
assert(n >= 0), 'n must be <= 0'
return 0 if n == 0 else n + nsum(n-1)
@memoize
def fibonacci(n):
'''返回斐波那契數列的第n個數'''
assert(n >= 0), 'n must be >= 0'
return n if n in (0, 1) else fibonacci(n-1) + fibonacci(n-2)
if __name__ == '__main__':
from timeit import Timer
measure = [ {'exec':'fibonacci(100)', 'import':'fibonacci', 'func':fibonacci},{'exec':'nsum(200)', 'import':'nsum', 'func':nsum} ]
for m in measure:
t = Timer('{}'.format(m['exec']), 'from __main__ import {}'.format(m['import']))
print('name: {}, doc: {}, executing: {}, time: {}'.format(m['func'].__name__, m['func'].__doc__, m['exec'], t.timeit()))
name: fibonacci, doc: 返回斐波那契數列的第n個數, executing: fibonacci(100), time: 0.272907609003596
name: nsum, doc: 返回前n個數字的和, executing: nsum(200), time: 0.2719842789811082
不錯!這一方案同時具備可讀的代碼和可接受的性能。此時,你可能想爭論說這不是修飾器 模式,因為我們并不是在運行時應用它。被修飾的函數確實無法取消修飾,但仍然可以在運行時 決定是否執行修飾器。這個有趣的練習就留給你來完成吧。
使用修飾器進行一層額外的封裝,基于某個條件來決定是否執行真正的修 飾器。
修飾器的另一個有趣的特性是可以使用多個修飾器來修飾一個函數。本章沒有涉及這一特 性,因此這是另一個練習,創建一個修飾器來幫助你調試遞歸函數,并將其應用于nsum()和 fibonacci()。多個修飾器會以什么次序執行?
如果你仍未充分理解修飾器,那么我有最后一個練習留給你。修飾器memoize()無法修飾接 受多個參數的函數。我們如何可以驗證這一點?驗證之后,嘗試找到一種方法解決這個問題: 經測試,memoize()對多參函數仍然有效。(此處可能有誤)
小結
本章介紹了修飾器模式及其與Python編程語言的關聯。我們使用修飾器模式來擴展一個對象的行為,無需使用繼承,非常方便。Python進一步擴展了修飾器的概念,允許我們無需使用繼承或組 合就能擴展任意可調用對象(函數、方法或類)的行為。我們可以使用Python內置的修飾器特性。
我們看了現實中一些被修飾對象的例子,比如槍和照相機。從軟件的視角來看,Django和Grok都使用了修飾器來達到不同的目標,比如控制HTTP壓縮和緩存。
修飾器模式是實現橫切關注點的絕佳方案,因為橫切關注點通用但不太適合使用面向對象編 程范式來實現。在5.3節中我們提到很多種橫切關注點。事實上,5.4節演示了一個橫切關注點, memoization。我們看到修飾器如何可以幫助我們保持函數簡潔,同時不犧牲性能。
本章中推薦的練習可以幫助你更好地理解修飾器,這樣你就能將這一強大工具用于解決許多 常見的(或許不太常見的)編程問題。第6章將介紹外觀模式,一種簡化復雜系統訪問的方式。
個人讀后感,好爛的一章,完全就是湊字數,還不如干脆挑明了直接解釋傳統意義上的裝飾器模式和python的裝飾器之間的差別,還有自己造了一個輪子:memorize,其實我們完全可以使用現有的輪子: from functools import lru_cache,還是別自己造輪子了。。
我后續會補充完整這方面的內容。
總結
以上是生活随笔為你收集整理的python 装饰器 继承_Python设计模式之装饰器模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 小程序 mathjs渲染公式_Mac 3
- 下一篇: repo同步代码_工欲善其事,必先利其器