《流畅的Python》 读书笔记 第7章_函数装饰器和闭包
第7章 函數(shù)裝飾器和閉包
裝飾器這個(gè)名稱可能更適合在編譯器領(lǐng)域使用,因?yàn)樗鼤?huì)遍歷并注解句法樹
函數(shù)裝飾器用于在源碼中“標(biāo)記”函數(shù),以某種方式增強(qiáng)函數(shù)的行為。這是一項(xiàng)強(qiáng)大的功能,但是若想掌握,必須理解
閉包
如果你想自己實(shí)現(xiàn)函數(shù)裝飾器,那就必須了解閉包的方方面面,因此也就需要知道 nonlocal
閉包還是回調(diào)式異步編程和函數(shù)式編程風(fēng)格的基礎(chǔ)
本章的最終目標(biāo)是解釋清楚函數(shù)裝飾器的工作原理,包括最簡(jiǎn)單的注冊(cè)裝飾器和較復(fù)雜的參數(shù)化裝飾器
討論如下話題
- Python 如何計(jì)算裝飾器句法
- Python 如何判斷變量是不是局部的
- 閉包存在的原因和工作原理
- nonlocal 能解決什么問題
再進(jìn)一步探討
- 實(shí)現(xiàn)行為良好的裝飾器
- 標(biāo)準(zhǔn)庫中有用的裝飾器
- 實(shí)現(xiàn)一個(gè)參數(shù)化裝飾器
7.1 裝飾器基礎(chǔ)知識(shí)
裝飾器是可調(diào)用的對(duì)象,其參數(shù)是另一個(gè)函數(shù)(被裝飾的函數(shù))
裝飾器可能會(huì)處理被裝飾的函數(shù),然后把它返回,或者將其替換成另一個(gè)函數(shù)或可調(diào)用對(duì)象
@decorate
def target():
print('running target()')
等價(jià)于
def target():
print('running target()')
target = decorate(target)
此處的decorate是你定義好的裝飾器,姑且認(rèn)為是個(gè)函數(shù)
這個(gè)函數(shù)被更改了,這也是網(wǎng)上流傳裝飾器萬能公式,記住了這點(diǎn)其實(shí)理解裝飾器或?qū)憘€(gè)簡(jiǎn)單的裝飾器是很容易的。
裝飾器的適用范圍非常廣泛,你可以參考《7.12 延伸閱讀- 關(guān)于裝飾器的一個(gè)典型應(yīng)用》
來看一個(gè)完整的例子
def deco(func):
def inner():
func() # 注: 此處我加的
print('running inner()')
return inner #?
@deco
def target(): #?
print('running target()')
target() #?
print(target) #?
? deco 返回 inner 函數(shù)對(duì)象。
? 使用 deco 裝飾 target。
? 調(diào)用被裝飾的 target 其實(shí)會(huì)運(yùn)行 inner。
? 審查對(duì)象,發(fā)現(xiàn) target 現(xiàn)在是 inner 的引用
執(zhí)行結(jié)果
running target()
running inner()
<function deco.<locals>.inner at 0x00000190D7E77D30>
可以看到如果target沒加這個(gè)裝飾器,肯定是單單執(zhí)行running target(),但加了裝飾器后
看似target執(zhí)行可以多出來running inner(),實(shí)際上此時(shí)的target已經(jīng)不再是原來的它了,它變了
根據(jù)萬能公式
@deco
def target():
pass
你這樣后會(huì)讓target變?yōu)?code>target = deco(target)
再根據(jù)deco的定義
def deco(func):
...
return inner #?
你在執(zhí)行deco(target)的時(shí)候,返回的是一個(gè)叫inner的東西
因?yàn)槟阕罱K執(zhí)行的是target(),所以也就是inner()
再看inner定義
def inner():
func()
print('running inner()')
inner()的時(shí)候,會(huì)執(zhí)行func(),func來自deco的實(shí)參,此處對(duì)應(yīng)target,所以你會(huì)先執(zhí)行target(),再執(zhí)行print('running inner()')
裝飾器只是語法糖
裝飾器可以像常規(guī)的可調(diào)用對(duì)象那樣調(diào)用,其參數(shù)是另一個(gè)函數(shù)。有時(shí),這樣做更方便,尤其是做元編程(在運(yùn)行時(shí)改變程序的行為)時(shí)
裝飾器的一大特性是,能把被裝飾的函數(shù)替換成其他函數(shù)
第二個(gè)特性是,裝飾器在加載模塊時(shí)立即執(zhí)行
7.2 Python何時(shí)執(zhí)行裝飾器
裝飾器的一個(gè)關(guān)鍵特性是,它們?cè)诒谎b飾的函數(shù)定義之后立即運(yùn)行。這通常是在導(dǎo)入時(shí)(即 Python 加載模塊時(shí))
書中的示例 registration.py 模塊
registry = [] #?
def register(func): #?
print('running register(%s)' % func) #?
registry.append(func) #?
return func #?
@register #?
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3(): #?
print('running f3()')
def main(): #?
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__=='__main__':
main() # ?
? registry 保存被 @register 裝飾的函數(shù)引用。
? register 的參數(shù)是一個(gè)函數(shù)。
? 為了演示,顯示被裝飾的函數(shù)。
? 把 func 存入 registry。
? 返回 func:必須返回函數(shù);這里返回的函數(shù)與通過參數(shù)傳入的一樣。
? f1 和 f2 被 @register 裝飾。
? f3 沒有裝飾。
? main 顯示 registry,然后調(diào)用 f1()、f2() 和 f3()。
? 只有把 registration.py 當(dāng)作腳本運(yùn)行時(shí)才調(diào)用 main()。
我做了一些測(cè)試
- 21行的main()不寫,直接就一個(gè)pass,也會(huì)執(zhí)行
running register(<function f1 at 0x000001940F3D7EE0>)
running register(<function f2 at 0x000001940F3F6040>)
- 這個(gè)跟你import 這個(gè)py文件的效果是一樣的,也充分說明了
在導(dǎo)入時(shí)立即運(yùn)行 - 這也是為何你在打印registry這個(gè)列表的時(shí)候已經(jīng)能看到里面有2個(gè)
- 類似的你把21行改為f1(),會(huì)打印如下。注意,有了上面的概念,你可能反而會(huì)覺得是不是會(huì)多打印一個(gè)
running register...,實(shí)則不然。
running register(<function f1 at 0x0000021998027E50>)
running register(<function f2 at 0x0000021998027D30>)
running f1()
- 最終寫上main()的運(yùn)行效果
running register(<function f1 at 0x000002A0F6CF7E50>)
running register(<function f2 at 0x000002A0F6CF7D30>)
running main()
registry -> [<function f1 at 0x000002A0F6CF7E50>, <function f2 at 0x000002A0F6CF7D30>]
running f1()
running f2()
running f3()
函數(shù)裝飾器在導(dǎo)入模塊時(shí)立即執(zhí)行,而被裝飾的函數(shù)只在明確調(diào)用時(shí)運(yùn)行。這突出了 Python 程序員所說的導(dǎo)入時(shí)和運(yùn)行時(shí)之間的區(qū)別
- 裝飾器函數(shù)與被裝飾的函數(shù)在同一個(gè)模塊中定義。實(shí)際情況是,裝飾器通常在一個(gè)模塊中定義,然后應(yīng)用到其他模塊中的函數(shù)上。
- register 裝飾器返回的函數(shù)與通過參數(shù)傳入的相同。實(shí)際上,大多數(shù)裝飾器會(huì)在內(nèi)部定義一個(gè)函數(shù),然后將其返回。
裝飾器原封不動(dòng)地返回被裝飾的函數(shù),但是這種技術(shù)并非沒有用處。很多 Python Web 框架使用這樣的裝飾器把函數(shù)添加到某種*注冊(cè)處,例如把URL 模式映射到生成 HTTP 響應(yīng)的函數(shù)上的注冊(cè)處。這種注冊(cè)裝飾器可能會(huì)也可能不會(huì)修改被裝飾的函數(shù)
7.3 使用裝飾器改進(jìn)“策略”模式
策略模式是第6章的內(nèi)容,比較的拗口,就先不寫了。
TODO
7.4 變量作用域規(guī)則
>>> def f(a): print(a)
... print(b)
File "<stdin>", line 2
print(b)
IndentationError: unexpected indent
>>> def f(a):
... print(a)
... print(b)
...
>>> f(1)
1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f
NameError: name 'b' is not defined
在書中,最后一行是這樣的
NameError: global name 'b' is not defined
雖然顯示不同(從Python3.5開始的),但的確b還是那個(gè)global,用生成的字節(jié)碼可以說明這點(diǎn)
>>> from dis import dis
>>> dis(f)
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 CALL_FUNCTION 1
6 POP_TOP
3 8 LOAD_GLOBAL 0 (print)
10 LOAD_GLOBAL 1 (b) # 看這里
12 CALL_FUNCTION 1
14 POP_TOP
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
加一個(gè)b的定義就能正常輸出了
>>> b=2
>>> f(1)
1
2
再看另外一個(gè)例子
>>> b = 1
>>> def func(a):
... print(a)
... print(b)
... b = 1
...
>>> func(2) 你可以思考下會(huì)輸出什么?為什么?
2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in func
UnboundLocalError: local variable 'b' referenced before assignment
你可能會(huì)覺得應(yīng)該打印b的值6,因?yàn)橥饷娑x了一個(gè)全局變量b,而在print(b)之后的b=9是后面執(zhí)行的, 不會(huì)打印9才是。
事實(shí)是,Python 編譯函數(shù)的定義體時(shí),它判斷 b 是局部變量,因?yàn)樵诤瘮?shù)中給它賦值了
也就是說在函數(shù)中加了一句b = 1,下面的就是b就從global變成了local variable
而且在函數(shù)外定義了全局變量b=1,這個(gè)函數(shù)是用不了的
從生成的字節(jié)碼看下
>>> from dis import dis
>>> dis(func)
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 CALL_FUNCTION 1
6 POP_TOP
3 8 LOAD_GLOBAL 0 (print)
10 LOAD_FAST 1 (b) # 這里
12 CALL_FUNCTION 1
14 POP_TOP
4 16 LOAD_CONST 1 (1)
18 STORE_FAST 1 (b)
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
10 LOAD_FAST 1 (b)這一行暴露了b是個(gè)local variable
這不是缺陷,而是設(shè)計(jì)選擇:Python 不要求聲明變量,但是假定在函數(shù)定義體中賦值的變量是局部變量
這比 JavaScript 的行為好多了,JavaScript 也不要求聲明變量,但是如果忘記把變量聲明為局部變量(使用 var),可能會(huì)在不知情的情況下獲取全局變量
b = 6
def fun(a):
global b
print(a)
print(b)
b=9
fun(3)
print(b)
這個(gè)global必須要在fun中定義
此時(shí)的字節(jié)碼
12 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 CALL_FUNCTION 1
6 POP_TOP
13 8 LOAD_GLOBAL 0 (print)
10 LOAD_GLOBAL 1 (b)
12 CALL_FUNCTION 1
14 POP_TOP
14 16 LOAD_CONST 1 (9)
18 STORE_GLOBAL 1 (b)
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
7.5 閉包
人們有時(shí)會(huì)把閉包和匿名函數(shù)弄混。這是有歷史原因的:在函數(shù)內(nèi)部定義函數(shù)不常見,直到開始使用匿名函數(shù)才會(huì)這樣做。而且,只有涉及嵌套函數(shù)時(shí)才有閉包問題。因此,很多人是同時(shí)知道這兩個(gè)概念的
閉包指延伸了作用域的函數(shù),其中包含函數(shù)定義體中引用、但是不在定義體中定義的非全局變量。
函數(shù)是不是匿名的沒有關(guān)系,關(guān)鍵是它能訪問定義體之外定義的非全局變量
書中的一個(gè)例子,要實(shí)現(xiàn)類似下面的效果
它的作用是計(jì)算不斷增加的系列值的均值;例如,整個(gè)歷史中某個(gè)商品的平均收盤價(jià)。每天都會(huì)增加新價(jià)格,因此平均值要考慮至目前為止所有的價(jià)格
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
乍一看這個(gè)題,你肯定會(huì)想到這是個(gè)函數(shù),這個(gè)函數(shù)傳入1個(gè)參數(shù),內(nèi)部有個(gè)東西可以記錄它的值,并計(jì)算出迄今為止所有數(shù)據(jù)的平均值
難道是這樣的?V1
def avg(value):
values = []
values.append(value)
return sum(values)/len(values)
print(avg(10))
print(avg(11))
顯然不對(duì),每次調(diào)用的時(shí)候values會(huì)被重新初始化成[],所以始終就一個(gè)值
難道是這樣的?V2
values = []
def avg(value):
values.append(value)
return sum(values)/len(values)
print(avg(10))
print(avg(11))
print(avg(12))
竟然對(duì)了,但是這values不能在外面啊,你拿到外面去算啥嗎~
上面是我寫的,來看作者寫的
class Averager():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total/len(self.series)
avg = Averager()
print(avg(10))
print(avg(11))
print(avg(12))
看到avg()你指應(yīng)該想到它是一個(gè)可調(diào)用對(duì)象,類實(shí)例也可以進(jìn)行調(diào)用,實(shí)現(xiàn)__call__就行啦
那在類這里你要實(shí)現(xiàn)這個(gè)代碼就簡(jiǎn)單了,上面的代碼應(yīng)該可以想通,跟我們之前的蹩腳代碼異曲同工。
來看看函數(shù)式的實(shí)現(xiàn):示例 7-9 average.py:計(jì)算移動(dòng)平均值的高階函數(shù)
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
avg = make_averager() # 你得到的是 averager 函數(shù)名這個(gè)一等對(duì)象
print(avg(10)) # averager(10) 就是平均值
print(avg(11))
print(avg(12))
書中給出的類和函數(shù)的實(shí)現(xiàn)有共通之處:調(diào)用 Averager() 或 make_averager() 得到一個(gè)可調(diào)用對(duì)象avg,它會(huì)更新歷史值,然后計(jì)算當(dāng)前均值
這個(gè)函數(shù)為何能進(jìn)行累加呢?當(dāng)然你能看得到這個(gè)寫法的特殊之處,函數(shù)里面有局部變量(series),又有內(nèi)部函數(shù)averager。但注意這個(gè)內(nèi)部函數(shù)用到了上面的局部變量
Averager 類的實(shí)例 avg 在哪里存儲(chǔ)歷史值很明顯:self.series 實(shí)例屬性。
但是第二個(gè)示例中的 avg 函數(shù)在哪里尋找 series 呢?
而且調(diào)用 avg(10) 時(shí),make_averager 函數(shù)已經(jīng)返回了,而它的本地作用域也一去不復(fù)返了。來看原文給的圖,我稍微擬合了下。
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
avg = make_averager()
avg(10)
avg(11)
avg(12)
# 審查返回的 averager 對(duì)象,我們發(fā)現(xiàn) Python 在 __code__ 屬性(表示編譯后的函數(shù)定義體)中保存局部變量和*變量的名稱
# 局部變量
print(avg.__code__.co_varnames)
# *變量
print(avg.__code__.co_freevars)
# avg.__closure__ 中的各個(gè)元素對(duì)應(yīng)于 avg.__code__.co_freevars 中的一個(gè)名稱。這些元素是 cell 對(duì)象,有個(gè) cell_contents 屬性,保存著真正的值
print(avg.__closure__)
print(avg.__closure__[0].cell_contents)
輸出
('new_value', 'total') # 局部變量
('series',) #*變量
(<cell at 0x00000197FA6B4FD0: list object at 0x00000197FA083240>,)#包含該函數(shù)可用變量的綁定的單元的元組
[10, 11, 12] # 單元的值
這里再說明下這幾個(gè)屬性的作用
| 屬性 | 作用 |
|---|---|
| co_varnames | 參數(shù)名和局部變量的元組 |
| co_freevars | *變量的名字組成的元組(通過函數(shù)閉包引用) |
__closesure__ |
None 或包含該函數(shù)可用變量的綁定的單元的元組。有關(guān) cell_contents 屬性的詳情見下。 |
| cell_contents | 單元對(duì)象具有 cell_contents 屬性。這可被用來獲取以及設(shè)置單元的值 |
引用自
https://docs.python.org/zh-cn/3.9/reference/datamodel.html?highlight=closure#the-standard-type-hierarchy
https://docs.python.org/zh-cn/3.9/library/inspect.html?highlight=inspect#module-inspect
avg.__closure__中的各個(gè)元素對(duì)應(yīng)于avg.__code__.co_freevars中的一個(gè)名稱。這些元素是 cell 對(duì)象,有個(gè) cell_contents 屬性,保存著真正的值
劃重點(diǎn): 閉包是一種函數(shù),它會(huì)保留定義函數(shù)時(shí)存在的*變量的綁定,這樣調(diào)用函數(shù)時(shí),雖然定義作用域不可用了,但是仍能使用那些綁定
注意,只有嵌套在其他函數(shù)中的函數(shù)才可能需要處理不在全局作用域中的外部變量
7.6 nonlocal聲明
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
在上面的做法中
實(shí)現(xiàn) make_averager 函數(shù)的方法效率不高
我們把所有值存儲(chǔ)在歷史數(shù)列中,然后在每次調(diào)用 averager 時(shí)使用 sum 求和
更好的實(shí)現(xiàn)方式是,只存儲(chǔ)目前的總值和元素個(gè)數(shù),然后使用這兩個(gè)數(shù)計(jì)算均值
書中也給了個(gè)示例,但是個(gè)陷阱,你還能看出來問題所在?
def make_averager():
total = 0
count = 0
def averager(new_value):
total += new_value
count += 1
return count/length
return averager
avg = make_averager()
avg(10)
在Pycharm中定義函數(shù)就是紅色的警告,會(huì)提示類似未解析的引用 'count' ,里面三行都紅的。
但執(zhí)行的時(shí)候會(huì)提示
Traceback (most recent call last):
File "demo_fluent7.py", line 10, in <module>
avg(10)
File "demo_fluent7.py", line 5, in averager
count += new_value
UnboundLocalError: local variable 'count' referenced before assignment
你上一次遇到它是在這里
>>> b = 1
>>> def func(a):
... print(a)
... print(b)
... b = 1
說明,這個(gè)count又成了一個(gè)局部變量?
看下dis
def make_averager():
...#省略
avg = make_averager()
from dis import dis
dis(avg)
輸出
5 0 LOAD_FAST 1 (count)
2 LOAD_FAST 0 (new_value)
4 INPLACE_ADD
6 STORE_FAST 1 (count)
6 8 LOAD_FAST 2 (total )
10 LOAD_CONST 1 (1)
12 INPLACE_ADD
14 STORE_FAST 2 (total )
7 16 LOAD_FAST 1 (count) # 看此處
18 LOAD_FAST 2 (total )
20 BINARY_TRUE_DIVIDE
22 RETURN_VALUE
為何會(huì)這樣呢?其實(shí)之前講過
當(dāng) count 是數(shù)字或任何不可變類型時(shí),count += 1 語句的作用其實(shí)與 count =count + 1 一樣。因此,我們?cè)?averager 的定義體中為 count 賦值了,
這會(huì)把 count 變成局部變量。total 變量也受這個(gè)問題影響。
當(dāng)你寫series = []的時(shí)候,我們利用了列表是可變的對(duì)象這一事實(shí),你在內(nèi)部函數(shù)體中只是做了series.append,這個(gè)對(duì)象并沒有改變
但是對(duì)數(shù)字、字符串、元組等不可變類型來說,只能讀取,不能更新。如果嘗試重新綁定,例如 count = count + 1,其實(shí)會(huì)隱式創(chuàng)建局部變量 count。這樣,count 就不是*變量了,因此不會(huì)保存在閉包中
這個(gè)細(xì)節(jié)在本書《第2章 數(shù)據(jù)結(jié)構(gòu)》的2.6 序列的增量賦值有描述,就是對(duì)數(shù)字而言,做count+=1的時(shí)候count不再是原來的count了
那是不是這樣的思路就不行了呢?倒也不是,就是稍微有點(diǎn)牽強(qiáng)
為了解決這個(gè)問題,Python 3 引入了 nonlocal 聲明。它的作用是把變量標(biāo)記為*變量,即使在函數(shù)中為變量賦予新值了,也會(huì)變成*變量。如果為 nonlocal 聲明的變量賦予新值,閉包中保存的綁定會(huì)更新
最終可以替代前面的例子的代碼如下
def make_averager():
total = 0
count = 0
def averager(new_value):
nonlocal count,total
count += 1
total += new_value
return total/count
return averager
avg = make_averager()
print(avg(10))
print(avg(11))
print(avg(12))
在沒有實(shí)現(xiàn)nonlocal的情況下(比如Python2中)
http://www.python.org/dev/peps/pep-3104/
PEP 3104—Access to Names inOuter Scopes
其中的第三個(gè)代碼片段給出了一種方法?;旧?,這種處理方式是把內(nèi)部函數(shù)需要修改的變量(如 count 和 total)存儲(chǔ)為可變對(duì)象(如字典或簡(jiǎn)單的實(shí)例)的元素或?qū)傩?,并且把那個(gè)對(duì)象綁定給一個(gè)*變量
7.7 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的裝飾器
示例 7-15 一個(gè)簡(jiǎn)單的裝飾器,輸出函數(shù)的運(yùn)行時(shí)間
import time
def clock(func):
def clocked(*args): # ?
t0 = time.perf_counter()
result = func(*args) # ?
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked # ?
@clock
def get_time():
from time import sleep
sleep(2)
get_time()
? 定義內(nèi)部函數(shù) clocked,它接受任意個(gè)定位注:位置參數(shù)。
? 這行代碼可用,是因?yàn)?clocked 的閉包中包含*變量 func。
? 返回內(nèi)部函數(shù),取代被裝飾的函數(shù)
關(guān)于第2點(diǎn),用代碼說明下
test = clock(get_time)
print(test.__code__.co_freevars) # ('func',)
示例 7-16 使用 clock 裝飾器
需要用到上面的代碼
import time
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
if __name__=='__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))
print(factorial.__name__)
執(zhí)行效果
**************************************** Calling snooze(.123)
[0.12786180s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000050s] factorial(1) -> 1
[0.00000770s] factorial(2) -> 2
[0.00001190s] factorial(3) -> 6
[0.00001650s] factorial(4) -> 24
[0.00002100s] factorial(5) -> 120
[0.00002730s] factorial(6) -> 720
6! = 720
clocked
工作原理
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
等價(jià)于
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
factorial = clock(factorial)
factorial成為了clock的實(shí)參,指向func形參;調(diào)用后clock(factorial)返回的是clocked
看上面我加的調(diào)試代碼print(factorial.__name__)得到的就是clocked
現(xiàn)在 factorial 保存的是 clocked 函數(shù)的引用。自此之后,每次調(diào)用 factorial(n),執(zhí)行的都是 clocked(n)。
代碼上clocked做了以下事情:
(1) 記錄初始時(shí)間 t0。 # t0 = time.perf_counter()
(2) 調(diào)用原來的 factorial 函數(shù),保存結(jié)果。 # result = func(*args) # ?
(3) 計(jì)算經(jīng)過的時(shí)間。 # elapsed = time.perf_counter() - t0
(4) 格式化收集的數(shù)據(jù),然后打印出來。
# 收集的數(shù)據(jù):包括前面的 elapsed 和 result
# name = func.__name__
# arg_str = ', '.join(repr(arg) for arg in args)
# print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
(5) 返回第 2 步保存的結(jié)果。 # return result
裝飾器的典型行為:
? 1. 把被裝飾的函數(shù)替換成新函數(shù)
? 2. 二者接受相同的參數(shù)
? 3. 而且(通常)返回被裝飾的函數(shù)本該返回的值
? 4. 同時(shí)還會(huì)做些額外操作
Gamma 等人寫的《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》一書是這樣概述“裝飾器”模式的:“動(dòng)態(tài)地給一個(gè)對(duì)象添加一些額外的職責(zé)?!焙瘮?shù)裝飾器符合這一說法。
但在實(shí)現(xiàn)層面,Python 裝飾器與《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》中所述的“裝飾器”沒有多少相似之處
示例 7-15 中實(shí)現(xiàn)的 clock 裝飾器有幾個(gè)缺點(diǎn):不支持關(guān)鍵字參數(shù),而且遮蓋了被裝飾函數(shù)的
__name__和__doc__屬性
import time
def clock(func):
'''doc of clock'''
def clocked(*args): # ?
'''doc of clocked'''
t0 = time.perf_counter()
result = func(*args) # ?
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked # ?
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
'''doc of fact'''
return 1 if n < 2 else n*factorial(n-1)
if __name__=='__main__':
print(factorial.__doc__)
print(factorial.__name__)
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
snooze(seconds=.123)
注意上面的代碼,做了一些更改
- 加了factorial、clock和clocked等函數(shù)的doc,你可以看到,
print(factorial.__doc__)輸出的是clocked的doc - 測(cè)試了下關(guān)鍵字輸入方式
snooze(seconds=.123)提示如下
TypeError: clocked() got an unexpected keyword argument 'seconds'
如果要支持關(guān)鍵字只需做如下更改
def clock(func):
'''doc of clock'''
def clocked(*args,**kwargs): # ?
'''doc of clocked'''
t0 = time.perf_counter()
result = func(*args,**kwargs) # ?
? clocked本身要支持**kwargs
? 內(nèi)部調(diào)用的時(shí)候要接受**kwargs
輸出大致如下
doc of clocked
clocked
**************************************** Calling snooze(.123)
[0.13518720s] snooze(0.123) -> None
[0.12407520s] snooze() -> None
問題1: 你可以看到,factorial的__doc__和__name__被遮擋了,這點(diǎn)在前面的萬能公式中我們也有提到
怎么處理呢?
使用 functools.wraps 裝飾器把相關(guān)的屬性從 func復(fù)制到 clocked 中
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args,**kwargs): # ?
t0 = time.perf_counter()
result = func(*args,**kwargs) # ?
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked # ?
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
'''doc of fact'''
return 1 if n < 2 else n*factorial(n-1)
if __name__=='__main__':
print(factorial.__doc__) # doc of fact
print(factorial.__name__) # factorial
可以看到__doc__和__name__改過來了
問題2:snooze(seconds=.123)這種調(diào)用方式在結(jié)果中沒有輸出參數(shù)
原因很簡(jiǎn)單,你沒有處理,你處理的只是args,你還要處理kwargs,參考代碼如下
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args,**kwargs): # ?
t0 = time.perf_counter()
result = func(*args,**kwargs) # ?
elapsed = time.perf_counter() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
arg_lst.append(', '.join(pairs))
arg_str = ''.join(arg_lst)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked # ?
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
'''doc of fact'''
return 1 if n < 2 else n*factorial(n-1)
if __name__=='__main__':
print(factorial.__doc__)
print(factorial.__name__)
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
snooze(seconds=.123)
7.8 標(biāo)準(zhǔn)庫中的裝飾器
Python 內(nèi)置了三個(gè)用于裝飾方法的函數(shù):property、classmethod 和 staticmethod
另一個(gè)常見的裝飾器是 functools.wraps,它的作用是協(xié)助構(gòu)建行為良好的裝飾器
標(biāo) 準(zhǔn) 庫 中 最 值 得 關(guān) 注 的 兩 個(gè) 裝 飾 器 是 lru_cache 和 全 新 的singledispatch(Python 3.4 新增)
7.8.1 使用functools.lru_cache做備忘
functools.lru_cache 是非常實(shí)用的裝飾器,它實(shí)現(xiàn)了備忘(memoization)功能。這是一項(xiàng)優(yōu)化技術(shù),它把耗時(shí)的函數(shù)的結(jié)果保存起來,避免傳入相同的參數(shù)時(shí)重復(fù)計(jì)算。LRU 三個(gè)字母是“Least Recently Used”的縮寫,表明緩存不會(huì)無限制增長,一段時(shí)間不用的緩存條目會(huì)被扔掉
示例 7-18 生成第 n 個(gè)斐波納契數(shù),遞歸方式非常耗時(shí)
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
if __name__=='__main__':
print(fibonacci(6))
這對(duì)你理解遞歸也是有幫助的
輸出如下(這個(gè)調(diào)用順序?qū)嵲谟悬c(diǎn)...)
[0.00000030s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00002610s] fibonacci(2) -> 1
[0.00000020s] fibonacci(1) -> 1
[0.00003430s] fibonacci(3) -> 2
[0.00000020s] fibonacci(1) -> 1
[0.00000020s] fibonacci(0) -> 0
[0.00000760s] fibonacci(2) -> 1
[0.00004960s] fibonacci(4) -> 3
[0.00000010s] fibonacci(1) -> 1
[0.00000020s] fibonacci(0) -> 0
[0.00000750s] fibonacci(2) -> 1
[0.00000020s] fibonacci(1) -> 1
[0.00001490s] fibonacci(3) -> 2
[0.00007280s] fibonacci(5) -> 5
[0.00000020s] fibonacci(1) -> 1
[0.00000020s] fibonacci(0) -> 0
[0.00000750s] fibonacci(2) -> 1
[0.00000010s] fibonacci(1) -> 1
[0.00001470s] fibonacci(3) -> 2
[0.00000020s] fibonacci(1) -> 1
[0.00000020s] fibonacci(0) -> 0
[0.00000750s] fibonacci(2) -> 1
[0.00002930s] fibonacci(4) -> 3
[0.00010970s] fibonacci(6) -> 8
8
畫個(gè)圖
從圖上可以看到,這里存在大量的重復(fù)操作
增加兩行代碼,使用 lru_cache,性能會(huì)顯著改善
@functools.lru_cache()
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
if __name__=='__main__':
print(fibonacci(6))
注意@functools.lru_cache()必須放@clock前面
這時(shí)候的輸出就是這樣,重復(fù)的調(diào)用都沒了
[0.00000040s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00002740s] fibonacci(2) -> 1
[0.00003230s] fibonacci(3) -> 2
[0.00003680s] fibonacci(4) -> 3
[0.00004120s] fibonacci(5) -> 5
[0.00004570s] fibonacci(6) -> 8
8
另外一個(gè)注意的點(diǎn)是:必須像常規(guī)函數(shù)那樣調(diào)用 lru_cache,后面有個(gè)()
作者做了個(gè)測(cè)試,可以看出,提升是巨大的
示例 7-19 中的版本(加了lru_cache的)在 0.0005 秒內(nèi)調(diào)用了 31 次fibonacci 函數(shù)
示例 7-18 中未緩存版本調(diào)用 fibonacci 函數(shù) 2 692 537 次,在使用Intel Core i7 處理器的筆記本電腦中耗時(shí) 17.7 秒
lru_cache簽名
functools.lru_cache(maxsize=128, typed=False)
- maxsize指定存儲(chǔ)多少個(gè)調(diào)用的結(jié)果,緩存滿了之后,舊的結(jié)果會(huì)被扔掉,騰出空間。為了得到最佳性能,maxsize 應(yīng)該設(shè)為 2 的冪
- typed 參數(shù)如果設(shè)為 True,把不同參數(shù)類型得到的結(jié)果分開保存,即把通常認(rèn)為相等的浮點(diǎn)數(shù)和整數(shù)參數(shù)區(qū)分開
- lru_cache 使用字典存儲(chǔ)結(jié)果,而且鍵根據(jù)調(diào)用時(shí)傳入的定位參數(shù)和關(guān)鍵字參數(shù)創(chuàng)建,所以被 lru_cache 裝飾的函數(shù),
它的所有參數(shù)都必須是可散列的,即不可變的
7.8.2 單分派泛函數(shù)
背景: 假設(shè)我們?cè)陂_發(fā)一個(gè)調(diào)試 Web 應(yīng)用的工具,我們想生成 HTML,顯示不同類型的 Python對(duì)象
import html
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
想改造這個(gè)函數(shù),以期達(dá)到下面的效果
- str:把內(nèi)部的換行符替換為
<br>\n';不使用<pre>,而是使用<p>。 - int:以十進(jìn)制和十六進(jìn)制顯示數(shù)字。
- list:輸出一個(gè) HTML 列表,根據(jù)各個(gè)元素的類型進(jìn)行格式化
示例 7-20 生成 HTML 的 htmlize 函數(shù),調(diào)整了幾種對(duì)象的輸出
>>> htmlize({1, 2, 3}) ?
'<pre>{1, 2, 3}</pre>'
>>> htmlize(abs)
'<pre><built-in function abs></pre>'
>>> htmlize('Heimlich & Co.\n- a game') ?
'<p>Heimlich & Co.<br>\n- a game</p>'
>>> 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>
? 默認(rèn)情況下,在 <pre></pre> 中顯示 HTML 轉(zhuǎn)義后的對(duì)象字符串表示形式。
? 為 str 對(duì)象顯示的也是 HTML 轉(zhuǎn)義后的字符串表示形式,不過放在 <p></p> 中,而且使用 <br> 表示換行。
? int 顯示為十進(jìn)制和十六進(jìn)制兩種形式,放在<pre></pre> 中。
? 各個(gè)列表項(xiàng)目根據(jù)各自的類型格式化,整個(gè)列表則渲染成 HTML 列表。
因?yàn)?Python 不支持重載方法或函數(shù),所以我們不能使用不同的簽名定義 htmlize 的變體,也無法使用不同的方式處理不同的數(shù)據(jù)類型。
重載overload,java中可以。Python只有override(重寫)。
不同的簽名是不支持的
def htmlize(obj:int):
pass
def htmlize(obj:str):
pass
def htmlize(obj:list):
pass
但我知道的一種做法是可以用第三方庫,類似于這樣
from multipledispatch import dispatch
@dispatch(int)
def htmlize(obj):
print('int')
@dispatch(str)
def htmlize(obj):
print('str')
@dispatch(list)
def htmlize(obj):
print('list')
htmlize(1) # int
htmlize('1') # str
htmlize([1,]) # list
htmlize((1,)) # 報(bào)錯(cuò)
# NotImplementedError: Could not find signature for htmlize: <tuple>
書中還說了一句也無法使用不同的方式處理不同的數(shù)據(jù)類型,我沒太理解,不是可以用isinstance來處理嗎?莫非在寫的時(shí)候
沒有這個(gè)玩意
第二版英文原文如下
Because we don’t have Java-style method overloading in Python, we can’t simply cre‐ate variations of htmlize with different signatures for each data type we want to han‐dle differently
在 Python 中,一種常見的做法是把 htmlize變成一個(gè)分派函數(shù),使用一串 if/elif/elif,調(diào)用專門的函數(shù),如 htmlize_str、htmlize_int,等等。這樣不便于模塊的用戶擴(kuò)展,還顯得笨拙:時(shí)間一長,分派函數(shù) htmlize 會(huì)變
得很大,而且它與各個(gè)專門函數(shù)之間的耦合也很緊密
書中給出的示例 7-21 singledispatch 創(chuàng)建一個(gè)自定義的 htmlize.register 裝飾器,把多個(gè)函數(shù)綁在一起組成一個(gè)泛函數(shù)
from functools import singledispatch
from collections import abc
import numbers
import html
@singledispatch # ?
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) # ?
@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>'
# 測(cè)試數(shù)據(jù)
print(htmlize({1, 2, 3}))
print(htmlize(abs))
print(htmlize('Heimlich & Co.\n- a game') )
print(htmlize(42) )
print(htmlize(['alpha', 66, {3, 2, 1}]))
? @singledispatch 標(biāo)記處理 object 類型的基函數(shù)。
? 各個(gè)專門函數(shù)使用 @?base_function?.register(?type?) 裝飾。
? 專門函數(shù)的名稱無關(guān)緊要;_ 是個(gè)不錯(cuò)的選擇,簡(jiǎn)單明了。
? 為每個(gè)需要特殊處理的類型注冊(cè)一個(gè)函數(shù)。numbers.Integral 是 int 的虛擬超類。
? 可以疊放多個(gè) register 裝飾器,讓同一個(gè)函數(shù)支持不同類型。
只要可能,注冊(cè)的專門函數(shù)應(yīng)該處理抽象基類(如 numbers.Integral 和 abc.MutableSequence),不要處理具體實(shí)現(xiàn)(如 int 和 list)。這樣,代碼支持的兼容類型更廣泛。例如,Python擴(kuò)展可以子類化 numbers.Integral,使用固定的位數(shù)實(shí)現(xiàn) int 類型
使用抽象基類檢查類型,可以讓代碼支持這些抽象基類現(xiàn)有和未來的具體子類或虛擬子類
singledispatch 機(jī)制的一個(gè)顯著特征是,你可以在系統(tǒng)的任何地方和任何模塊中注冊(cè)專門函數(shù)。
如果后來在新的模塊中定義了新的類型,可以輕松地添加一個(gè)新的專門函數(shù)來處理那個(gè)類型。
此外,你還可以為不是自己編寫的或者不能修改的類添加自定義函數(shù)。
singledispatch 是經(jīng)過深思熟慮之后才添加到標(biāo)準(zhǔn)庫中的,它提供的特性很多 , 詳見
PEP 443 — Single-dispatch generic functions
https://www.python.org/dev/peps/pep-0443/
@singledispatch 不是為了把 Java 的那種方法重載帶入 Python。在一個(gè)類中為同一個(gè)方法定義多個(gè)重載變體,比在一個(gè)函數(shù)中使用一長串 if/elif/elif/elif 塊要更好。但是這兩種方案都有缺陷,因?yàn)樗鼈冏尨a單元(類
或函數(shù))承擔(dān)的職責(zé)太多。@singledispath 的優(yōu)點(diǎn)
是支持模塊化擴(kuò)展:各個(gè)模塊可以為它支持的各個(gè)類型注冊(cè)一個(gè)專門函數(shù)
7.9 疊放裝飾器
裝飾器是函數(shù),因此可以組合起來使用(即,可以在已經(jīng)被裝飾的函數(shù)上應(yīng)用裝飾器)
前面已經(jīng)多次這樣使用,比如
@functools.lru_cache()
@clock
def fibonacci(n):
pass
但要注意順序
@d1
@d2
def f():
print('f')
等價(jià)于f = d1(d2(f)),就近原則,最近@的最先裝飾
7.10 參數(shù)化裝飾器
解析源碼中的裝飾器時(shí),Python 把被裝飾的函數(shù)作為第一個(gè)參數(shù)傳給裝飾器函數(shù)。
那怎么讓裝飾器接受其他參數(shù)呢?
答案是:創(chuàng)建一個(gè)裝飾器工廠函數(shù),把參數(shù)傳給它,返回一個(gè)裝飾器,然后再把它應(yīng)用到要裝飾的函數(shù)上
書中給了個(gè)示例示例 7-22 示例 7-2 中 registration.py 模塊的刪減版
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
print('running main()')
print('registry ->', registry)
f1()
7.10.1 一個(gè)參數(shù)化的注冊(cè)裝飾器
為了便于啟用或禁用 register 執(zhí)行的函數(shù)注冊(cè)功能,我們?yōu)樗峁┮粋€(gè)可選的 active 參數(shù),設(shè)為 False 時(shí),不注冊(cè)被裝飾的函數(shù)
從概念上看,這個(gè)新的 register 函數(shù)不是裝飾器,而是裝飾器工廠函數(shù)。調(diào)用它會(huì)返回真正的裝飾器,這才是應(yīng)用到目標(biāo)函數(shù)上的裝飾器。
示例 7-23 為了接受參數(shù),新的 register 裝飾器必須作為函數(shù)調(diào)用
registry = set() #?
def register(active=True): #?
def decorate(func): #?
print('running register(active=%s)->decorate(%s)'% (active, func))
if active: #?
registry.add(func)
else:
registry.discard(func) #?
return func #?
return decorate #?
@register(active=False) #?
def f1():
print('running f1()')
@register() #?
def f2():
print('running f2()')
def f3():
print('running f3()')
? registry 現(xiàn)在是一個(gè) set 對(duì)象,這樣添加和刪除函數(shù)的速度更快。
? register 接受一個(gè)可選的關(guān)鍵字參數(shù)。
? decorate 這個(gè)內(nèi)部函數(shù)是真正的裝飾器;注意,它的參數(shù)是一個(gè)函數(shù)。
? 只有 active 參數(shù)的值(從閉包中獲取)是 True 時(shí)才注冊(cè) func。
? 如果 active 不為真,而且 func 在 registry 中,那么把它刪除。
? decorate 是裝飾器,必須返回一個(gè)函數(shù)。
? register 是裝飾器工廠函數(shù),因此返回 decorate。然后把它應(yīng)用到被裝飾的函數(shù)上
? @register 工廠函數(shù)必須作為函數(shù)調(diào)用,并且傳入所需的參數(shù)。
? 即使不傳入?yún)?shù),register 也必須作為函數(shù)調(diào)用(@register()),即要返回真正的裝飾器 decorate
在終端下你可以測(cè)試出以下結(jié)果,假設(shè)文件是demo.py
>>> import demo
running register(active=False)->decorate(<function f1 at 0x000002860CF2CEE0>)
running register(active=True)->decorate(<function f2 at 0x000002860CF2CF70>)
>>> demo.registry
{<function f2 at 0x000002860CF2CF70>}
跟之前7.2說的一樣導(dǎo)入的時(shí)候就會(huì)執(zhí)行
只有 f2 函數(shù)在 registry 中;f1 不在其中,因?yàn)閭鹘o register 裝飾器工廠函數(shù)的參數(shù)是 active=False,所以應(yīng)用到 f1 上的 decorate 沒有把它添加到 registry 中
如果不使用 @ 句法,那就要像常規(guī)函數(shù)那樣使用 register;若想把 f 添加到 registry中,則裝飾 f 函數(shù)的句法是 register()(f);不想添加(或把它刪除)的話,句法是register(active=False)(f)
上面這部分是關(guān)鍵
@clock
def fibonacci(n):
pass
你知道,fibonacci=clock(fibonacci)
那你現(xiàn)在要做的是
@clock(param='xxx')
def fibonacci(n):
pass
那自然fibonacci=clock(param='xxx')(fibonacci)
所以你應(yīng)該定義一個(gè)
def clock(param='xxx'):
pass
而這個(gè)clock的返回需要是一個(gè)函數(shù),參數(shù)應(yīng)該是一個(gè)函數(shù)(比如fibonacci)
def clock(param='xxx'):
def decorate(func):
pass
decorate
書中還給你做了下如下測(cè)試
>>> from registration_param import * # 我上面的測(cè)試改了此處的名字 demo
running register(active=False)->decorate(<function f1 at 0x10073c1e0>)
running register(active=True)->decorate(<function f2 at 0x10073c268>)
>>> registry # ?
{<function f2 at 0x10073c268>}
>>> register()(f3) # ?
running register(active=True)->decorate(<function f3 at 0x10073c158>)
<function f3 at 0x10073c158>
>>> registry # ?
{<function f3 at 0x10073c158>, <function f2 at 0x10073c268>}
>>> register(active=False)(f2) # ?
running register(active=False)->decorate(<function f2 at 0x10073c268>)
<function f2 at 0x10073c268>
>>> registry # ?
{<function f3 at 0x10073c158>}
? 導(dǎo)入這個(gè)模塊時(shí),f2 在 registry 中。
? register() 表達(dá)式返回 decorate,然后把它應(yīng)用到 f3 上。
? 前一行把 f3 添加到 registry 中。
? 這次調(diào)用從 registry 中刪除 f2。
? 確認(rèn) registry 中只有 f3。
7.10.2 參數(shù)化clock裝飾器
上面的裝飾器比較簡(jiǎn)單,但通常參數(shù)化裝飾器的原理相當(dāng)復(fù)雜,
參數(shù)化裝飾器通常會(huì)把被裝飾的函數(shù)替換掉,而且結(jié)構(gòu)上需要多一層嵌套。
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT): # ?
def decorate(func): # ?
def clocked(*_args): # ?
t0 = time.time()
_result = func(*_args) # ?
elapsed = time.time() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args) # ?
result = repr(_result) # ?
print(fmt.format(**locals())) # ?
return _result # ?
return clocked # ?
return decorate # ?
? clock 是參數(shù)化裝飾器工廠函數(shù)。
? decorate 是真正的裝飾器。
? clocked 包裝被裝飾的函數(shù)。
? _result 是被裝飾的函數(shù)返回的真正結(jié)果。
? _args 是 clocked 的參數(shù),args 是用于顯示的字符串。
? result 是 _result 的字符串表示形式,用于顯示。
? 這里使用**locals()是為了在 fmt 中引用 clocked 的局部變量。
? clocked 會(huì)取代被裝飾的函數(shù),因此它應(yīng)該返回被裝飾的函數(shù)返回的值。
? decorate 返回 clocked。
? clock 返回 decorate。在這個(gè)模塊中測(cè)試,不傳入?yún)?shù)調(diào)用 clock(),因此應(yīng)用的裝飾器使用默認(rèn)的格式 str。應(yīng)該是DEFAULT_FMT
**locals()** 函數(shù)會(huì)以字典類型返回當(dāng)前位置的全部局部變量,配合fmt來用,還是挺巧妙的~
locals: {'_args': (0.123,), 't0': 1699234406.3928096, '_result': None, 'elapsed': 0.12681794166564941, 'name': 'snooze', 'args': '0.123', 'result': 'None', 'fmt': '[{elapsed:0.8f}s] {name}({args}) -> {result}', 'func': <function snooze at 0x0000026ED4107F70>}
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}' # 在上面也有
另外一點(diǎn)就是
參數(shù)化裝飾器通常會(huì)把被裝飾的函數(shù)替換掉,而且結(jié)構(gòu)上需要多一層嵌套。
考慮上面的結(jié)構(gòu)
def clock(fmt=DEFAULT_FMT):
def decorate(func):
def clocked(*_args):
...
return _result
return clocked
return decorate
@clock()
def snooze(seconds):
pass
結(jié)合萬能公式
snooze=clock()(snooze) #注意此處的第一個(gè)()
snooze=decorate(snooze) # 轉(zhuǎn)換下
snooze=clocked # 替換了
最終
for i in range(3):
snooze(.123)
就相當(dāng)于
for i in range(3):
clocked(.123)
所以下面的幾個(gè)測(cè)試結(jié)果
測(cè)試1
if __name__ == '__main__':
@clock()
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
輸出
[0.13555145s] snooze(0.123) -> None
[0.12589598s] snooze(0.123) -> None
[0.12798786s] snooze(0.123) -> None
測(cè)試2
if __name__ == '__main__':
@clock('{name}: {elapsed}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
輸出
snooze: 0.12915396690368652s
snooze: 0.1259920597076416s
snooze: 0.1258389949798584s
測(cè)試3
if __name__ == '__main__':
@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
輸出
snooze(0.123) dt=0.126s
snooze(0.123) dt=0.126s
snooze(0.123) dt=0.126s
Graham Dumpleton 和 Lennart Regebro(本書的技術(shù)審校之一)認(rèn)為,
裝飾器最好通過實(shí)現(xiàn) __call__ 方法的類實(shí)現(xiàn),不應(yīng)該像本章的示例那樣通過函數(shù)實(shí)現(xiàn)
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
class Clock:
def __init__(self,fmt=DEFAULT_FMT):
self.fmt = fmt
def __call__(self, func):
def clocked(*_args):
t0 = time.time()
_result = func(*_args)
elapsed = time.time() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args)
result = repr(_result)
#print('locals:',locals())
print(self.fmt.format(**locals()))
return _result
return clocked
if __name__ == '__main__':
@Clock()
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
@Clock('{name}: {elapsed}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
@Clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
同樣的推導(dǎo)
snooze=Clock()(snooze)
其中Clock()是個(gè)實(shí)例,假設(shè)為clock_instance
那clock_instance(snnoze)就是在調(diào)用__call__,返回的就是clocked,也發(fā)生了替換
從寫法上更讓清晰一些
7.11 本章小結(jié)
從本章開始進(jìn)入元編程領(lǐng)域
開始,我們先編寫了一個(gè)沒有內(nèi)部函數(shù)的 @register 裝飾器;最后,我們實(shí)現(xiàn)了有兩層嵌套函數(shù)的參數(shù)化裝飾器 @clock()
參數(shù)化裝飾器基本上都涉及至少兩層嵌套函數(shù),如果想使用 @functools.wraps 生成裝飾器,為高級(jí)技術(shù)提供更好的支持,嵌套層級(jí)可能還會(huì)更深,比如前面簡(jiǎn)要介紹過的疊放裝飾器
討論了標(biāo)準(zhǔn)庫中 functools 模塊提供的兩個(gè)出色的函數(shù)裝飾器:@lru_cache() 和@singledispatch
若想真正理解裝飾器,需要區(qū)分導(dǎo)入時(shí)和運(yùn)行時(shí),還要知道變量作用域、閉包和新增的nonlocal 聲明。掌握閉包和 nonlocal 不僅對(duì)構(gòu)建裝飾器有幫助,還能協(xié)助你在構(gòu)建 GUI程序時(shí)面向事件編程,或者使用回調(diào)處理異步 I/O
7.12 延伸閱讀
| 素材 | URL | 相關(guān)信息 |
|---|---|---|
| Python Cookbook(第 3 版)中文版》第 9 章“元編程” | 有幾個(gè)訣竅構(gòu)建了基本的裝飾器和特別復(fù)雜的裝飾器 9.6 定義一個(gè)能接收可選參數(shù)的裝飾器”一節(jié)中的裝飾器可以作為常規(guī)的裝飾器調(diào)用,也可以作為裝飾器工廠函數(shù)調(diào)用,例如 @clock 或 @clock() |
|
| Graham Dumpleton 博 客 文 章 | https://github.com/GrahamDumpleton/wrapt/blob/develop/blog/README.md | 深入剖析了如何實(shí)現(xiàn)行為良好的裝飾器 |
| How You Implemented Your Python Decorator is Wrong | https://github.com/GrahamDumpleton/wrapt/blob/develop/blog/01-how-you-implemented-your-python-decorator-is-wrong.md | |
| wrapt 模塊 | http://wrapt.readthedocs.org/en/latest | 這個(gè)模塊的作用是簡(jiǎn)化裝飾器和動(dòng)態(tài)函數(shù)包裝器的實(shí)現(xiàn),即使多層裝飾也支持內(nèi)省,而且行為正確,既可以應(yīng)用到方法上,也可以作為描述符使用 |
| Michele Simionato的decorator包 | https://pypi.python.org/pypi/decorator | 簡(jiǎn)化普通程序員使用裝飾器的方式,并且通過各種復(fù)雜的示例推廣裝飾器 |
| Python Decorator Library 維基頁面 | https://wiki.python.org/moin/PythonDecoratorLibrary | 里面有很多示例 |
| PEP 443 | http://www.python.org/dev/peps/pep-0443 | 對(duì)單分派泛函數(shù)的基本原理和細(xì)節(jié)做了說明 |
| Five-Minute Multimethods in Python | http://www.artima.com/weblogs/viewpost.jsp?thread=101605 | 詳細(xì)說明了如何使用裝飾器實(shí)現(xiàn)泛函數(shù)(也叫多方法),他給出的代碼支持多分派(即根據(jù)多個(gè)定位參數(shù)進(jìn)行分派) |
| Martijn Faassen 開發(fā)的 Reg | http://reg.readthedocs.io/en/latest/ | 如果想使用現(xiàn)代 的技術(shù)實(shí)現(xiàn)多分派泛函數(shù),并支持在生產(chǎn)環(huán)境中使用,可以用 它 |
| Fredrik Lundh 寫的一篇短文Closures in Python | http://effbot.org/zone/closure.htm | 解說了閉包這個(gè)術(shù)語 |
| PEP 3104—Access to Names in Outer Scopes | http://www.python.org/dev/peps/pep-3104 | 說明了引入 nonlocal 聲明的原因:重新綁定既不在本地作用域中也不在全局作用域中的名稱。這份 PEP 還概述了其他動(dòng)態(tài)語言(Perl、Ruby、JavaScript,等等)解決這個(gè)問題的方式,以及 Python 中可用設(shè)計(jì)方案的優(yōu)缺點(diǎn) |
| PEP 227—Statically Nested Scopes | http://www.python.org/dev/peps/pep-0227/ | 說明了 Python 2.1 引入的詞法作用域;這份 PEP 還說明了 Python 中閉包的基本原理和實(shí)現(xiàn)方式的選擇 |
雜談
-
任何把函數(shù)當(dāng)作一等對(duì)象的語言,它的設(shè)計(jì)者都要面對(duì)一個(gè)問題:作為一等對(duì)象的函數(shù)在某個(gè)作用域中定義,但是可能會(huì)在其他作用域中調(diào)用。問題是,如何計(jì)算*變量?首先出現(xiàn)的最簡(jiǎn)單的處理方式是使用“動(dòng)態(tài)作用域”。也就是說,根據(jù)函數(shù)調(diào)用所在的環(huán)境計(jì)算*變量。
-
動(dòng)態(tài)作用域易于實(shí)現(xiàn),這可能就是 John McCarthy 創(chuàng)建 Lisp(第一門把函數(shù)視作一等對(duì)象的語言)時(shí)采用這種方式的原因
-
Python 函數(shù)裝飾器符合 Gamma 等人在《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》一書中對(duì)“裝飾器”模式的一般描述:“動(dòng)態(tài)地給一個(gè)對(duì)象添加一些額外的職責(zé)。就擴(kuò)展功能而言,裝飾器模式比子類化更靈活。”
-
在設(shè)計(jì)模式中,Decorator 和 Component 是抽象類。為了給具體組件添加行為,具體裝飾器的實(shí)例要包裝具體組件的實(shí)例
-
裝飾器與它所裝飾的組件接口一致,因此它對(duì)使用該組件的客戶透明。它將客戶請(qǐng)求轉(zhuǎn)發(fā)給該組件,并且可能在轉(zhuǎn)發(fā)前后執(zhí)行一些額外的操作(例如繪制一個(gè)邊框)。透明性使得你可以遞歸嵌套多個(gè)裝飾器,從而可以添加任意多的功能
-
一般來說,實(shí)現(xiàn)“裝飾器”模式時(shí)最好使用類表示裝飾器和要包裝的組件
還有很多,不再一一羅列了啦,雜談部分就當(dāng)看Python歷史了
關(guān)于裝飾器的一個(gè)典型應(yīng)用
引自 劉江的博客
有一個(gè)大公司,下屬的基礎(chǔ)平臺(tái)部負(fù)責(zé)內(nèi)部應(yīng)用程序及API的開發(fā)。另外還有上百個(gè)業(yè)務(wù)部門負(fù)責(zé)不同的業(yè)務(wù),這些業(yè)務(wù)部門各自調(diào)用基礎(chǔ)平臺(tái)部提供的不同函數(shù),也就是API處理自己的業(yè)務(wù),情況如下:
# 基礎(chǔ)平臺(tái)部門開發(fā)了上百個(gè)函數(shù)API
def f1():
print("業(yè)務(wù)部門1的數(shù)據(jù)接口......")
def f2():
print("業(yè)務(wù)部門2的數(shù)據(jù)接口......")
def f3():
print("業(yè)務(wù)部門3的數(shù)據(jù)接口......")
def f100():
print("業(yè)務(wù)部門100的數(shù)據(jù)接口......")
#各部門分別調(diào)用自己需要的API
f1()
f2()
f3()
f100()
公司還在創(chuàng)業(yè)初期時(shí),基礎(chǔ)平臺(tái)部就開發(fā)了這些函數(shù)。由于各種原因,比如時(shí)間緊,比如人手不足,比如架構(gòu)缺陷,比如考慮不周等等,沒有為函數(shù)的調(diào)用進(jìn)行安全認(rèn)證?,F(xiàn)在,公司發(fā)展壯大了,不能再像初創(chuàng)時(shí)期的“草臺(tái)班子”一樣將就下去了,基礎(chǔ)平臺(tái)部主管決定彌補(bǔ)這個(gè)缺陷,于是(以下場(chǎng)景純屬虛構(gòu),調(diào)侃之言,切勿對(duì)號(hào)入座):
第一天:主管叫來了一個(gè)運(yùn)維工程師,工程師跑上跑下逐個(gè)部門進(jìn)行通知,讓他們?cè)诖a里加上認(rèn)證功能,然后,當(dāng)天他被開除了。
第二天:主管又叫來了一個(gè)運(yùn)維工程師,工程師用shell寫了個(gè)復(fù)雜的腳本,勉強(qiáng)實(shí)現(xiàn)了功能。但他很快就回去接著做運(yùn)維了,不會(huì)開發(fā)的運(yùn)維不是好運(yùn)維....
第三天:主管叫來了一個(gè)python自動(dòng)化開發(fā)工程師。哥們是這么干的,只對(duì)基礎(chǔ)平臺(tái)的代碼進(jìn)行重構(gòu),讓N個(gè)業(yè)務(wù)部門無需做任何修改。這哥們很快也被開了,連運(yùn)維也沒得做?! ?/p>
def f1():
#加入認(rèn)證程序代碼
print("業(yè)務(wù)部門1數(shù)據(jù)接口......")
def f2():
# 加入認(rèn)證程序代碼
print("業(yè)務(wù)部門2數(shù)據(jù)接口......")
def f3():
# 加入認(rèn)證程序代碼
print("業(yè)務(wù)部門3數(shù)據(jù)接口......")
def f100():
#加入認(rèn)證程序代碼
print("業(yè)務(wù)部門100數(shù)據(jù)接口......")
#各部門分別調(diào)用
f1()
f2()
f3()
f100()
第四天:主管又換了個(gè)開發(fā)工程師。他是這么干的:定義個(gè)認(rèn)證函數(shù),在原來其他的函數(shù)中調(diào)用它,代碼如下。
def login():
print("認(rèn)證成功!")
def f1():
login()
print("業(yè)務(wù)部門1數(shù)據(jù)接口......")
def f2():
login()
print("業(yè)務(wù)部門2數(shù)據(jù)接口......")
def f3():
login()
print("業(yè)務(wù)部門3數(shù)據(jù)接口......")
def f100():
login()
print("業(yè)務(wù)部門100數(shù)據(jù)接口......")
#各部門分別調(diào)用
f1()
f2()
f3()
f100()
但是主管依然不滿意,不過這一次他解釋了為什么。主管說:寫代碼要遵循開放封閉原則,簡(jiǎn)單來說,已經(jīng)實(shí)現(xiàn)的功能代碼內(nèi)部不允許被修改,但外部可以被擴(kuò)展。如果將開放封閉原則應(yīng)用在上面的需求中,那么就是不允許在函數(shù)f1 、f2、f3......f100的內(nèi)部進(jìn)行代碼修改,但是可以在外部對(duì)它們進(jìn)行擴(kuò)展。
第五天:已經(jīng)沒有時(shí)間讓主管找別人來干這活了,他決定親自上陣,使用裝飾器完成這一任務(wù),并且打算在函數(shù)執(zhí)行后再增加個(gè)日志功能。主管的代碼如下:
def outer(func):
def inner():
print("認(rèn)證成功!")
result = func()
print("日志添加成功")
return result
return inner
@outer
def f1():
print("業(yè)務(wù)部門1數(shù)據(jù)接口......")
@outer
def f2():
print("業(yè)務(wù)部門2數(shù)據(jù)接口......")
@outer
def f3():
print("業(yè)務(wù)部門3數(shù)據(jù)接口......")
@outer
def f100():
print("業(yè)務(wù)部門100數(shù)據(jù)接口......")
#各部門分別調(diào)用
f1()
f2()
f3()
f100()
使用裝飾器@outer,也是僅需對(duì)基礎(chǔ)平臺(tái)的代碼進(jìn)行拓展,就可以實(shí)現(xiàn)在其他部門調(diào)用函數(shù)API之前都進(jìn)行認(rèn)證操作,在操作結(jié)束后保存日志,并且其他業(yè)務(wù)部門無需對(duì)他們自己的代碼做任何修改,調(diào)用方式也不用變
總結(jié)
以上是生活随笔為你收集整理的《流畅的Python》 读书笔记 第7章_函数装饰器和闭包的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ubuntu部署雷池Waf社区版
- 下一篇: Python MySQL 数据库查询:选