1. 一個簡單的裝飾器
定義了一個裝飾器,它會在每次調用被裝飾的函數時計時,然後把經過的時間、傳入的參數和調用的結果打印出來。
# a3_4_decorate.py
import time
def clock(func):
def clocked(*args): # 定義內部函數 clocked
t0 = time.perf_counter()
res = func(*args) # 這行代碼可用,是因爲 clocked 的閉包中包含自由變量 func
elapsed = time.perf_counter() - t0
name = func.__name__
args_str = ', '.join(repr(arg) for arg in args)
print(f'[{elapsed:>2.8}s], {name}({args_str})--> {res}')
return res
return clocked # 返回內部函數,取代被裝飾的函數。
# a3_4_decorate_func.py
import time
from a3_4_decorate import clock
@clock
def test_sleep(seconds):
time.sleep(seconds)
@clock
def test_factorial(n):
return 1 if n < 2 else n * test_factorial(n-1)
if __name__ == '__main__':
test_sleep(1.2)
test_factorial(6)
# [1.2008935s], test_sleep(1.2)--> None
# [2e-06s], test_factorial(1)--> 1
# [0.0001048s], test_factorial(2)--> 2
# [0.0001478s], test_factorial(3)--> 6
# [0.0001863s], test_factorial(4)--> 24
# [0.0002236s], test_factorial(5)--> 120
# [0.0002705s], test_factorial(6)--> 720
在示例中,test_factorial 會作爲 func 參數傳給 clock。然後,clock 函數會返回 clocked 函數,Python 解釋器在背後會把 clocked 賦值給 test_factorial 。其實,導入 clockdeco_demo 模塊後查看 test_factorial 的 __name__
屬性:
# a3_4_decorate_func_import.py
import a3_4_decorate_func
print(a3_4_decorate_func.test_factorial.__name__)
print(a3_4_decorate_func.test_sleep.__name__)
# clocked
# clocked
所以,現在 test_factorial 保存的是 clocked 函數的引用。自此之後,每次調用 test_factorial(n),執行的都是 clocked(n)。
這是裝飾器的典型行爲:把被裝飾的函數替換成新函數,二者接受相同的參數,而且(通常)返回被裝飾的函數本該返回的值,同時還會做些額外操作。
本例中實現的 clock 裝飾器有幾個缺點:不支持關鍵字參數,而且遮蓋了被裝飾函數的__name__
和__doc__
屬性。下面將使用 functools.wraps 裝飾器把相關的屬性從 func複製到 clocked 中。
# a3_4_decorate_wraps.py
import time
from functools import wraps
def clock(func):
@wraps(func)
def clocked(*args, **kwargs):
t0 = time.perf_counter()
res = func(*args, **kwargs)
elapsed = time.perf_counter() - t0
name = func.__name__
args_list = []
if args:
args_list.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = [f'{k}={v}' for k, v in sorted(kwargs.items())]
args_list.append(', '.join(pairs))
args_str = ', '.join(args_list)
print(f'[{elapsed:>2.8}s], {name}({args_str})--> {res}')
return res
return clocked
# a3_4_decorate_wraps_run.py
import time
from a3_4_decorate_wraps import clock
@clock
def test_sleep(seconds, name=None):
time.sleep(seconds)
@clock
def test_factorial(n, name="fi"):
return 1 if n < 2 else n * test_factorial(n - 1)
if __name__ == '__main__':
test_sleep(1.2, name='xiaoming')
test_factorial(6, name='fi')
# [1.200119s], test_sleep(1.2, name=xiaoming)--> None
# [1.7e-06s], test_factorial(1)--> 1
# [6.01e-05s], test_factorial(2)--> 2
# [8.98e-05s], test_factorial(3)--> 6
# [0.0001158s], test_factorial(4)--> 24
# [0.0001468s], test_factorial(5, name=fi)--> 120
2. 標準庫中的裝飾器
Python 內置了三個用於裝飾方法的函數:property、classmethod 和 staticmethod。
另一個常見的裝飾器是 functools.wraps,它的作用是協助構建行爲良好的裝飾器。
標 準 庫 中 最 值 得 關 注 的 兩 個 裝 飾 器 是 lru_cache 和全新的singledispatch(Python 3.4 新增)。這兩個裝飾器都在 functools 模塊中定義。
2.1 使用functools.lru_cache做備忘
functools.lru_cache 是非常實用的裝飾器,它實現了備忘(memoization)功能。這是一項優化技術,它把耗時的函數的結果保存起來,避免傳入相同的參數時重複計算。LRU 三個字母是“Least Recently Used”的縮寫,表明緩存不會無限制增長,一段時間不用的緩存條目會被扔掉。
生成第 n 個斐波納契數這種慢速遞歸函數適合使用 lru_cache:
from a3_4_decorate import clock
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__=='__main__':
print(fibonacci(6))
'''
[9e-07s], fibonacci(0)--> 0
[1.2e-06s], fibonacci(1)--> 1
[9.52e-05s], fibonacci(2)--> 1
[8e-07s], fibonacci(1)--> 1
[1e-06s], fibonacci(0)--> 0
[9e-07s], fibonacci(1)--> 1
[4.64e-05s], fibonacci(2)--> 1
[9.12e-05s], fibonacci(3)--> 2
[0.0002324s], fibonacci(4)--> 3
3
'''
這裏生成第 n 個斐波納契數,遞歸方式非常耗時,fibonacci(0) 調用了 2 次,fibonacci(1) 調用了 3 次……但是,
如果增加兩行代碼,使用 lru_cache,使用緩存實現,速度更快,性能會顯著改善。
from functools import lru_cache
from a3_4_decorate import clock
@lru_cache()
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__=='__main__':
print(fibonacci(4))
'''
[1.6e-06s], fibonacci(0)--> 0
[2e-06s], fibonacci(1)--> 1
[0.0001693s], fibonacci(2)--> 1
[3.3e-06s], fibonacci(3)--> 2
[0.0002637s], fibonacci(4)--> 3
3
'''
需要注意的是:必須像常規函數那樣調用 lru_cache。這一行中有一對括號:@functools.lru_cache()。這麼做的原因是,lru_cache 可以接受配置參數。另外,這裏疊放了裝飾器:@lru_cache() 應用到 @clock 返回的函數上。
特別要注意,lru_cache 可以使用兩個可選的參數來配置。它的簽名是:functools.lru_cache(maxsize=128, typed=False):maxsize 參數指定存儲多少個調用的結果。緩存滿了之後,舊的結果會被扔掉,騰出空間。爲了得到最佳性能,maxsize 應該設爲 2 的冪。typed 參數如果設爲 True,把不同參數類型得到的結果分開保存,即把通常認爲相等的浮點數和整數參數(如 1 和 1.0)區分開。順便說一下,因爲 lru_cache 使用字典存儲結果,而且鍵根據調用時傳入的定位參數和關鍵字參數創建,所以被 lru_cache 裝飾的函數,它的所有參數都必須是可散列的。
2.2 單分派泛函數
假設我們在開發一個調試 Web 應用的工具,我們想生成 HTML,顯示不同類型的 Python對象。
import html
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
這個函數適用於任何 Python 類型,但是現在我們想做個擴展,讓它使用特別的方式顯示某些類型。
-
str:把內部的換行符替換爲
'<br>\n'
;不使用<pre>
,而是使用<p>
。 -
int:以十進制和十六進制顯示數字。
-
list:輸出一個 HTML 列表,根據各個元素的類型進行格式化。
因爲 Python 不支持重載方法或函數,所以我們不能使用不同的簽名定義 htmlize 的變體,也無法使用不同的方式處理不同的數據類型。在 Python 中,一種常見的做法是把 htmlize變成一個分派函數,使用一串 if/elif/elif,調用專門的函數,如 htmlize_str、htmlize_int,等等。這樣不便於模塊的用戶擴展,還顯得笨拙:時間一長,分派函數 htmlize 會變得很大,而且它與各個專門函數之間的耦合也很緊密。
Python 3.4 新增的 functools.singledispatch 裝飾器可以把整體方案拆分成多個模塊,甚至可以爲你無法修改的類提供專門的函數。使用 @singledispatch 裝飾的普通函數會變成泛函數(generic function):根據第一個參數的類型,以不同方式執行相同操作的一組函數。
# singledispatch 創建一個自定義的 htmlize.register 裝飾器,
# 把多個函數綁在一起組成一個泛函數
from functools import singledispatch
import numbers
import html
from collections import abc
@singledispatch # @singledispatch 標記處理 object 類型的基函數
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
@htmlize.register(str)
def _(text): # 專門函數的名稱無關緊要;_ 是個不錯的選擇,簡單明瞭。
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)
@htmlize.register(numbers.Integral)
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)
@htmlize.register(tuple) # 可以疊放多個register裝飾器,讓同一個函數支持不同類型
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'
print(htmlize({1, 2, 3})) # <pre>{1, 2, 3}</pre>
print(htmlize(abs)) # <pre><built-in function abs></pre>
print(htmlize('Heimlich & Co.\n- a game'))
# <p>Heimlich & Co.<br>
# - a game</p>
print(htmlize(42)) # <pre>42 (0x2a)</pre>
print(htmlize(['alpha', 66, {3, 2, 1}]))
# <ul>
# <li><p>alpha</p></li>
# <li><pre>66 (0x42)</pre></li>
# <li><pre>{1, 2, 3}</pre></li>
# </ul>
只要可能,註冊的專門函數應該處理抽象基類(如 numbers.Integral 和 abc.MutableSequence),不要處理具體實現(如 int 和 list)。這樣,代碼支持的兼容類型更廣泛。例如,Python擴展可以子類化numbers.Integral,使用固定的位數實現 int 類型。
singledispatch 機制的一個顯著特徵是,你可以在系統的任何地方和任何模塊中註冊專門函數。如果後來在新的模塊中定義了新的類型,可以輕鬆地添加一個新的專門函數來處理那個類型。此外,你還可以爲不是自己編寫的或者不能修改的類添加自定義函數。
3. 疊放裝飾器
把 @d1 和 @d2 兩個裝飾器按順序應用到 f 函數上,作用相當於 f = d1(d2(f))。
def d1(func):
def decorate():
pass
return decorate
def d2(func):
def decorate():
pass
return decorate
@d1
@d2
def f():
print('f')
# 等同於:
def f():
print('f')
f = d1(d2(f))
4. 參數化裝飾器
解析源碼中的裝飾器時,Python 把被裝飾的函數作爲第一個參數傳給裝飾器函數。那怎麼讓裝飾器接受其他參數呢?答案是:創建一個裝飾器工廠函數,把參數傳給它,返回一個裝飾器,然後再把它應用到要裝飾的函數上。
registry = set()
def register(active=True):
def decorate(func): # decorate 這個內部函數是真正的裝飾器;它的參數是一個函數。
print(f'running register(active={active})->decorate({func})')
if active:
registry.add(func)
else:
registry.discard(func)
return func # decorate 是裝飾器,必須返回一個函數。
return decorate # register 是裝飾器工廠函數,因此返回 decorate。
@register(active=False) # 爲了接受參數,新的register裝飾器必須作爲函數調用。
def f1():
print('running f1()')
@register()
def f2():
print('running f2()')
def f3():
print('running f3()')
if __name__ == '__main__':
print(f'registry:{registry}')
f1()
f2()
f3()
print(f'registry:{registry}')
'''
running register(active=False)->decorate(<function f1 at 0x000001EDB675B378>)
running register(active=True)->decorate(<function f2 at 0x000001EDB675B400>)
registry:{<function f2 at 0x000001EDB675B400>}
running f1()
running f2()
running f3()
registry:{<function f2 at 0x000001EDB675B400>}
'''
@register 工廠函數必須作爲函數調用,並且傳入所需的參數。即使不傳入參數,register 也必須作爲函數調用(@register()),即要返回真正的裝飾器 decorate。
關鍵是,register() 要返回 decorate,然後把它應用到被裝飾的函數上。
如果不使用 @ 句法,那就要像常規函數那樣使用 register;若想把 f 添加到 registry中,則裝飾 f 函數的句法是 register()(f);不想添加(或把它刪除)的話,句法是register(active=False)(f)。
from a3_5_decorate_parameter import register, registry, f1, f2, f3
# running register(active=False)->decorate(<function f1 at 0x000002183A36B400>)
# running register(active=True)->decorate(<function f2 at 0x000002183A36B488>)
print(registry)
# {<function f2 at 0x000002183A36B488>}
register()(f3)
# running register(active=True)->decorate(<function f3 at 0x000002183A36B378>)
print(registry)
# {<function f2 at 0x000002183A36B488>, <function f3 at 0x000002183A36B378>}
register(active=False)(f2)
# running register(active=False)->decorate(<function f2 at 0x000002183A36B488>)
print(registry)
# {<function f3 at 0x000002183A36B378>}
5. 參數化clock裝飾器
爲clock添加一個功能:讓用戶傳入一個格式字符串,控制被裝飾函數的輸出。
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT): # clock 是參數化裝飾器工廠函數。
def decorate(func): # decorate 是真正的裝飾器
def clocked(*args): # clocked 包裝被裝飾的函數
t0 = time.time()
_res = func(*args)
result = repr(_res)
elapsed = time.time() - t0
name = func.__name__
args_str = ', '.join(repr(arg) for arg in args)
print(fmt.format(**locals())) # 使用 **locals() 是爲了在 fmt 中引用 clocked 的局部變量
return _res
return clocked
return decorate
if __name__ == '__main__':
@clock()
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
'''
[0.12307549s] snooze((0.123,)) -> None
[0.12305403s] snooze((0.123,)) -> None
[0.12399721s] snooze((0.123,)) -> None
'''
@clock('{name}: {elapsed}s')
def snooze(seconds):
time.sleep(seconds)
# snooze: 0.12392497062683105s
@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
time.sleep(seconds)
# snooze((0.123,)) dt=0.123s