本文的文字及圖片來源于網(wǎng)絡(luò),僅供學(xué)習(xí)郁轻、交流使用,不具有任何商業(yè)用途,如有問題請(qǐng)及時(shí)聯(lián)系我們以作處理
最近閱讀《流暢的python》看見其用函數(shù)寫裝飾器部分寫的很好,想寫一些自己的讀書筆記躏仇。眾所周知昙楚,裝飾器是python學(xué)習(xí)過程中的一道門檻,初學(xué)者學(xué)習(xí)時(shí)往往是知其然溃肪,不知其所以然争涌,這樣的結(jié)果是導(dǎo)致一段時(shí)間后會(huì)遺忘掉該部分內(nèi)容粉楚,只好再次去學(xué)習(xí),拉高了學(xué)習(xí)成本。
想學(xué)好python的裝飾器模软,需要明白一下幾點(diǎn)伟骨;
1:閉包
1)函數(shù)嵌套
2)內(nèi)部函數(shù)使用外部函數(shù)的變量
3)外部函數(shù)的返回值為內(nèi)部函數(shù)
? 接下來看看《流暢的python》中的例子,我稍微修改了一下:
>>> 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
<function make_averager.<locals>.averager at 0x10b82cb00>
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
? 函數(shù) make_averager 實(shí)現(xiàn)了一個(gè) 計(jì)算當(dāng)前所有數(shù)字的平均值的功能燃异,不斷的添加一個(gè)值携狭,然后計(jì)算當(dāng)前的平均值。
? avg這個(gè)對(duì)象內(nèi)存地址指向了make_averager這個(gè)函數(shù)的內(nèi)部函數(shù)中回俐,而且avg通過不斷的添加值進(jìn)行平均值計(jì)算逛腿,按理說在這個(gè)內(nèi)部函數(shù)沒有存儲(chǔ)new_value的空間,而且在make_averager對(duì)avg賦值后仅颇,函數(shù)返回后series這個(gè)變量也應(yīng)該消失了单默,但是avg卻依然可以進(jìn)行計(jì)算。
? 這就是閉包忘瓦,內(nèi)部函數(shù)averager使用外面的自由變量搁廓,也就是屬于make_averager的局部變量series
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
? 可以發(fā)現(xiàn)avg的自由變量是make_averager的局部變量,就是說閉包里的內(nèi)部函數(shù)可以使用外部函數(shù)的變量耕皮,即我們上面提到的第二點(diǎn):“內(nèi)部函數(shù)使用外部函數(shù)的變量”境蜕, 注:自由變量只能read,并不能write明场,不然會(huì)提示本地變量并沒有賦值的錯(cuò)誤,我們舉的例子沒遇到這個(gè)問題李丰,因?yàn)槲覀儧]有給 series 賦值苦锨,我們只是調(diào) 用 series.append,并把它傳給 sum 和 len趴泌。也就是說舟舒,我們利用了 列表是可變的對(duì)象這一事實(shí) 。下圖是書中提供的閉包范圍圖:
?
2:裝飾器的實(shí)現(xiàn)
所謂裝飾器嗜憔,就是在不改變基礎(chǔ)函數(shù)的功能上再次給它封裝一層秃励,達(dá)到我們想要的目的,接下來我舉個(gè)簡單的例子:
? deco_demo.py
1 def col(func):
? 2? ? def inner(*args, **kwargs):
? 3? ? ? ? print(func.__name__)
? 4? ? ? ? print(locals())
? 5? ? ? ? print(inner.__code__.co_varnames)
? 6? ? ? ? print(inner.__code__.co_freevars)
? 7? ? ? ? return func(*args, **kwargs)
? 8? ? return inner
? 9
10
11 @col
12 def new_add(x):
13? ? return x+2
14
15
16 def new_add_1(x):
17? ? return x+3
18
19
20 print(new_add(3))
21
22 new_add_1 = col(new_add_1)
23 print(new_add_1(3))
下方是它的返回結(jié)果:
new_add
{'args': (3,), 'kwargs': {}, 'func': <function new_add at 0x10d32aa70>, 'inner': <function col.<locals>.inner at 0x10d32acb0>}
('args', 'kwargs')
('func', 'inner')
5
new_add_1
{'args': (3,), 'kwargs': {}, 'func': <function new_add_1 at 0x10d32add0>, 'inner': <function col.<locals>.inner at 0x10d32a8c0>}
('args', 'kwargs')
('func', 'inner')
6
1-8:是定義的一個(gè)簡單裝飾器吉捶,
3:打印當(dāng)被裝飾函數(shù)的名字
4:打印inner這個(gè)內(nèi)部函數(shù)中的所有變量
5:打印當(dāng)前inner的局部變量夺鲜;
6:則打印自由變量;
11-13:修飾了一個(gè)簡單函數(shù)
16呐舔,22币励,23:@這個(gè)語法糖,背后實(shí)現(xiàn)的過程珊拼;
? 也就是說col(new_add)返回的是當(dāng)前的內(nèi)部函數(shù)的內(nèi)存地址食呻,而這個(gè)調(diào)用這個(gè)內(nèi)部函數(shù)時(shí)會(huì)使用自由變量func即col的局部變量,進(jìn)而達(dá)到裝飾器的目的;
有參數(shù)的裝飾器實(shí)現(xiàn)
? 既然無參數(shù)的裝飾器即@col 仅胞,通過內(nèi)部函數(shù)的方式裝飾基礎(chǔ)函數(shù)每辟,那么我們調(diào)用有參數(shù)的裝飾器 則可以再原本的基礎(chǔ)即函數(shù)col再封裝一層函數(shù),使其達(dá)到可以通過裝飾器傳參數(shù)的目的
1 from functools import wraps
? 2
? 3
? 4 def col(string="hello world"):
? 5? ? def decorate(func):
? 6? ? ? ? @wraps(func)
? 7? ? ? ? def inner(*args, **kwargs):
? 8? ? ? ? ? ? print(string)
? 9? ? ? ? ? ? return func(*args, **kwargs)
10? ? ? ? return inner
11? ? return decorate
12
13
14 @col()
15 def new_add(x):
16? ? return x+2
17
18
19 @col("hello python")
20 def new_add_1(x):
21? ? return x+3
22
23
24 def new_add_2(x):
25? ? return x+4
26
27
28 print(new_add(1))
29 print(new_add_1(1))
30
31
32 new_add_2 = col("hello china")(new_add_2)
33 print(new_add_2(1))
導(dǎo)入wrap是為了修復(fù)這個(gè)裝飾器的名稱干旧,new_add.__name__調(diào)用時(shí)指向被裝飾的函數(shù)渠欺,而不是內(nèi)部函數(shù),有興趣的小伙伴可以去了解一下莱革;
4-11:實(shí)現(xiàn)了一個(gè)帶參數(shù)的裝飾器峻堰,最外層返回的是我們真正的裝飾器;
32-33:則是@這個(gè)裝飾器語法糖背后的實(shí)現(xiàn)過程
可以發(fā)現(xiàn)new_add與new_add_1這兩個(gè)函數(shù)的裝飾器是兩個(gè)不同值盅视,而我們的裝飾器也返回了不同的對(duì)應(yīng)情況
hello world
3
hello python
4
hello china
5
間而言之:裝飾器就是在我們需要添加功能的函數(shù)上進(jìn)而封裝一層捐名,而python的語法糖@背后,幫助我們省略掉了這些賦值的過程闹击;
3:裝飾器何時(shí)調(diào)用
關(guān)于裝飾器何時(shí)運(yùn)行镶蹋,我們分兩種情況討論,一種是當(dāng)作腳本運(yùn)行時(shí)赏半,另一種是當(dāng)作模塊被導(dǎo)入時(shí)贺归;
1 registry = []
? 2
? 3
? 4 def register(func):
? 5? ? print(f"running register {func}")
? 6? ? registry.append(func)
? 7? ? return func
? 8
? 9
10 @register
11 def f1():
12? ? print('running f1()')
13
14
15 @register
16 def f2():
17? ? print('running f2()')
18
19
20 def f3():
21? ? print('running f3()')
22
23
24 def main():
25? ? print('running main()')
26? ? print('regisry ->', registry)
27? ? f1()
28? ? f2()
29? ? f3()
30
31
32 if __name__ == '__main__':
33? ? main()
當(dāng)作獨(dú)立腳本運(yùn)行時(shí):
running register <function f1 at 0x103f9dcb0>
running register <function f2 at 0x103f9ddd0>
running main()
regisry -> [<function f1 at 0x103f9dcb0>, <function f2 at 0x103f9ddd0>]
running f1()
running f2()
running f3()
被當(dāng)作模塊導(dǎo)入時(shí):
>>> import registration
running register <function f1 at 0x1005a2710>
running register <function f2 at 0x1005a2b90>
>>> registration.registry
[<function f1 at 0x1005a2710>, <function f2 at 0x1005a2b90>]
該段代碼的裝飾器主要功能是:記錄了被裝飾函數(shù)的個(gè)數(shù),通常是web框架以這種方式把函數(shù)注冊(cè)到中央注冊(cè)器的某處断箫。
總結(jié):可以發(fā)現(xiàn)裝飾器無論是作為模塊被導(dǎo)入拂酣,還是單獨(dú)的腳本運(yùn)行,它都是優(yōu)先執(zhí)行的仲义;
4:裝飾器的常用模塊
之前介紹的function.wraps不用說了婶熬,接下來介紹兩種神奇的裝飾器;
1:singledispatch
何為singledispatch ?
就是在不改變函數(shù)本身的功能上復(fù)用該函數(shù)埃撵,達(dá)到重復(fù)使用函數(shù)名的目的赵颅,有點(diǎn)類似多態(tài)的感覺;可以把整體方案拆分成多個(gè)模塊暂刘,甚至可以為你無法修改的類提供專門的函數(shù)饺谬。使用@singledispatch 裝飾的普通函數(shù)會(huì)變成泛函數(shù)(generic function);根據(jù)第一個(gè)參數(shù)的類型谣拣,以不同方式執(zhí)行相同操作的一組函數(shù)
1 from functools import singledispatch
? 2
? 3
? 4 @singledispatch
? 5 def hello(obj):
? 6? ? print(obj)
? 7
? 8
? 9 @hello.register(str)
10 def _(text):
11? ? print("hello world "+text)
12
13
14 @hello.register(int)
15 def _(n):
16? ? print(n)
17
18
19 hello({"what": "say"})
20 print('*'*30)
21 hello('dengxuan')
22 print('*'*30)
23 hello(123)
{'what': 'say'}
******************************
hello world dengxuan
******************************
123
從該段代碼中我們可以發(fā)現(xiàn)募寨,當(dāng)使用singledispatch這個(gè)裝飾器時(shí),函數(shù)hello可以根據(jù)不同的參數(shù)返回不同的結(jié)果森缠。這樣的好處就是極大的減少代碼中的if/elif/else绪商,并且可以復(fù)用函數(shù)名稱 _ (下橫線代表沒用),降低了代碼的耦合度辅鲸,達(dá)到了多態(tài)的效果格郁。
2:lru_cache
根據(jù)書上原話:
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ì)被扔掉。
1 from my_tools.runtime import clock
? 2 import functools
? 3
? 4
? 5 @functools.lru_cache()
? 6 @clock
? 7 def fibonacci(n):
? 8? ? if n < 2:
? 9? ? ? ? return n
10? ? return fibonacci(n-2)+fibonacci(n-1)
11
12
13 if __name__ == '__main__':
14? ? print(fibonacci(6))
第5行:注釋funtools.lru_cache()
返回結(jié)果:
[0.00000046] fibonacci(0) -> 0
[0.00000053] fibonacci(1) -> 1
[0.00006782] fibonacci(2) -> 1
[0.00000030] fibonacci(1) -> 1
[0.00000035] fibonacci(0) -> 0
[0.00000037] fibonacci(1) -> 1
[0.00001312] fibonacci(2) -> 1
[0.00002514] fibonacci(3) -> 2
[0.00010535] fibonacci(4) -> 3
[0.00000030] fibonacci(1) -> 1
[0.00000030] fibonacci(0) -> 0
[0.00000037] fibonacci(1) -> 1
[0.00001209] fibonacci(2) -> 1
[0.00002376] fibonacci(3) -> 2
[0.00000028] fibonacci(0) -> 0
[0.00000038] fibonacci(1) -> 1
[0.00001210] fibonacci(2) -> 1
[0.00000028] fibonacci(1) -> 1
[0.00000036] fibonacci(0) -> 0
[0.00000034] fibonacci(1) -> 1
[0.00001281] fibonacci(2) -> 1
[0.00002466] fibonacci(3) -> 2
[0.00004897] fibonacci(4) -> 3
[0.00008414] fibonacci(5) -> 5
[0.00020196] fibonacci(6) -> 8
8
當(dāng)取消掉第5行注釋時(shí);
[0.00000040] fibonacci(0) -> 0
[0.00000049] fibonacci(1) -> 1
[0.00008032] fibonacci(2) -> 1
[0.00000066] fibonacci(3) -> 2
[0.00009398] fibonacci(4) -> 3
[0.00000063] fibonacci(5) -> 5
[0.00010943] fibonacci(6) -> 8
8
可以發(fā)現(xiàn)树瞭,lru_cache()這個(gè)裝飾器拇厢,極大的提高了計(jì)算性能;
maxsize 參數(shù)指定存儲(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ù)(如 1 和 1.0)區(qū)分開。順 便說一下蓝撇,因?yàn)?lru_cache 使用字典存儲(chǔ)結(jié)果果复,而且鍵根據(jù)調(diào)用時(shí)傳 入的定位參數(shù)和關(guān)鍵字參數(shù)創(chuàng)建,所以被 lru_cache 裝飾的函數(shù)渤昌,它的所有參數(shù)都必須是可散列的虽抄。
5:多重裝飾器
@d1
@d2
def f():
? print('hello world')
###########################
def f():
? print("hello world")
f = d1(d2(f))
上下兩塊代碼是等效效果;
想要獲取更多Python學(xué)習(xí)資料可以加
QQ:2955637827私聊
或加Q群630390733
大家一起來學(xué)習(xí)討論吧耘沼!