python函数装饰器一篇入魂
一、什么是裝飾器
裝飾器顧名思義就是裝飾的工具,指的是在不改變被裝飾對象的源代碼以及調(diào)用方式的前提下,為被裝飾對象增加額外的功能,裝飾器經(jīng)常使用在以下幾個場景,比如:插入日志、性能測試、事務(wù)處理、緩存、權(quán)限校驗、測試等應(yīng)用場景
二、為何要有裝飾器
軟件的設(shè)計應(yīng)該遵循開放封閉原則,即對擴(kuò)展是開放的,而對修改是封閉的。我們對現(xiàn)有的代碼需要擴(kuò)展功能時不能修改源代碼以及調(diào)用方式,否則稍有不慎,就會導(dǎo)致整個程序報錯,因為我們要修改的這段程序可能在很多地方調(diào)用,所以修改源代碼和調(diào)用方式是很不明智的。而裝飾器則可以在不修改源代碼和調(diào)用方式的情況為原程序拓展新的功能。
三、裝飾器的實現(xiàn)思路
現(xiàn)在有一段程序,用來計算0~n的和
def add(n):sum = 0for i in range(n):sum += ireturn sumres = add(100000) print(res)輸出: >> 4999950000如果想計算一下這個函數(shù)的運行時間,在修改源代碼的情況下我們是這樣做的
import timedef add(n):start = time.time()sum = 0for i in range(n):sum += istop = time.time()print('function run time: %s'%(stop - start))return sumres = add(100000) print(res)輸出: >> function run time: 0.014251947402954102 >> 4999950000上面的代碼雖然實現(xiàn)了為函數(shù)體擴(kuò)展計算函數(shù)運行時間的功能,但是違背了我們開頭所說的開放封閉原則,為函數(shù)增加新功能是不能修改源代碼的!那么在不修改源代碼的情況下,如何為其加新功能呢?我們可以在調(diào)用的時候,計算函數(shù)體運行的時間
import timedef add(n):sum = 0for i in range(n):sum += ireturn sumstart = time.time() res = add(100000) print(res) stop = time.time() print('function run time: %s'%(stop - start))輸出: >> 4999950000 >> function run time: 0.014489889144897461現(xiàn)在實現(xiàn)了在不修改源代碼以及調(diào)用方式的情況下,為原函數(shù)增加了計算運行時間的功能,既沒有違背開放封閉原則,也擴(kuò)展了新功能,看似是完美,但是有沒有想過,我們可能在很多地方都調(diào)用了add函數(shù),那我們要在每次調(diào)用的時候都寫這么一堆代碼?這完全是重復(fù)且冗余的代碼,說到重復(fù)冗余的代碼,有些同學(xué)就想到了,我們可以封裝成一個函數(shù)啊!嘖嘖~
import timedef add(n):sum = 0for i in range(n):sum += ireturn sumdef wrapper():start = time.time()res = add(100000)print(res)stop = time.time()print('function run time: %s'%(stop - start))res = wrapper() print(res)輸出: >> 4999950000 >> function run time: 0.015117883682250977 >> None好了,現(xiàn)在我們將拓展的功能封裝成一個函數(shù)了,這樣我們就省去了一大堆的重復(fù)冗余代碼,但是大家有沒有察覺到,上面的代碼是有問題的
第一個問題: add 函數(shù)是有返回值的,但是我們的新函數(shù)wrapper(這里可以稱之為裝飾器了)返回值是None。
第二個問題 :調(diào)用 add 函數(shù)是需要傳一個參數(shù)進(jìn)去的,而裝飾器函數(shù)卻沒有傳參,這不就違背了文章開頭提到的開放封閉原則了么
是的 所以我們還可以將函數(shù)優(yōu)化一下
解決第一個問題:在裝飾器函數(shù)里面要把調(diào)用add函數(shù)返回的值給return出來
解決第二個問題:在裝飾器函數(shù)里面,把調(diào)用add函數(shù)時傳的值參數(shù)化
至此,我們已經(jīng)在不違背開放封閉原則的前提下為 add 函數(shù)拓展了計算運行時間的功能,這樣就大功告成了么? 不不不,我們現(xiàn)在只是實現(xiàn)了為 add 函數(shù)擴(kuò)展了計算運行時間的功能而已,如果我們還需要為其他函數(shù)增加計算運行時間的功能怎么辦,難道要再寫一個嗎?裝飾器是通用的,所以我們要考慮一下,函數(shù)名能不能參數(shù)化?(函數(shù)對象的概念), 我們先嘗試一下把函數(shù)名參數(shù)化,看看會有什么問題~
import timedef add(n):sum = 0for i in range(n):sum += ireturn sumdef index(x,y):print('from index %s:%s'%(x,y))def wrapper(func,n):start = time.time()res = func(n)stop = time.time()print('function run time: %s'%(stop - start))return resres_add = wrapper(add,10000) print(res_add)wrapper(index,1,2)輸出: >> function run time: 0.0007648468017578125 >> 49995000File "/Users/qts/PycharmProjects/QTS/test/ll.py", line 45, in <module>res_index = wrapper(index,1,2) TypeError: wrapper() takes 2 positional arguments but 3 were given將函數(shù)名參數(shù)化以后,新增index函數(shù)進(jìn)行測試一下,調(diào)用wrapper為index函數(shù)增加擴(kuò)展功能,出現(xiàn)了問題,什么問題呢?我們來看一下
第一個問題:index 函數(shù)是需要傳兩個參數(shù)的,但是裝飾器函數(shù)wrapper 只有一個位置形參可以傳值,另一個參數(shù)怎么傳?
首先解決第一個問題,被裝飾的對象有幾個參數(shù)我們不知道,那么我們可不可以用不定長參數(shù)來接收呢,這里就用到args,**kwags來接收傳進(jìn)來的參數(shù)打包成一個元組或者字典,然后在函數(shù)體內(nèi)使用args,**kwargs 把元組或者字典重新打散傳值給func,這樣就實現(xiàn)了我們給wrapper傳什么參數(shù),func接收的就是什么參數(shù)
import timedef add(n):sum = 0for i in range(n):sum += ireturn sumdef index(x,y):print('from index %s:%s'%(x,y))def wrapper(func,*args,**kwargs):start = time.time()res = func(*args,**kwargs)stop = time.time()print('function run time: %s'%(stop - start))return resres_add = wrapper(add,10000) print(res_add)wrapper(index,1,2)輸出: >> function run time: 0.0012209415435791016 >> 49995000 >> from index 1:2 >> function run time: 1.0013580322265625e-05第二個問題:我們在調(diào)用的時候多傳了一個形參func,因為我們需要通過func 傳入被裝飾的函數(shù)對象,裝飾器函數(shù)改動了形參,改變了原函數(shù)的調(diào)用方式,違背了開頭說的開放封閉原則
解決第二個問題就使用到了閉包函數(shù)的概念,不了解的小伙伴可以去百度一下,閉包函數(shù)其實就是第二種為函數(shù)體傳參的方式,通過閉包函數(shù)的方式我們解決了第二個問題,現(xiàn)在我們就已經(jīng)實現(xiàn)了完整的無參裝飾器了。
import timedef add(n):sum = 0for i in range(n):sum += ireturn sumdef index(x,y):print('from index %s:%s'%(x,y))def outter(func):def wrapper(*args,**kwargs):start = time.time()res = func(*args,**kwargs)stop = time.time()print('function run time: %s'%(stop - start))return resreturn wrapperadd = outter(add) res = add(10000) print(add)index = outter(index) index(2,1)輸出: >> function run time: 0.0017659664154052734 >> 49995000 >> from index 2:1 >> function run time: 1.621246337890625e-05另外提一點命名空間的概念,調(diào)用outter函數(shù)返回的是wrapper的函數(shù)對象,我們在全局作用域拿到后可以賦值給x,可以賦值給y,也可以賦值給原函數(shù)名,把原函數(shù)名指向函數(shù)對象給覆蓋,我們就像是在調(diào)用原函數(shù)名一樣調(diào)用裝飾器了,是不是很完美~,其實python就已經(jīng)幫我們實現(xiàn)這件事了,這里要介紹一個令人快樂的東西叫語法糖
語法糖
在被裝飾對象上面加上@裝飾器,就相當(dāng)于 add = outter(add) 然后我們調(diào)用add時,其實就是調(diào)用的outter函數(shù)返回的wrapper
再多說一句哈,現(xiàn)在我們實現(xiàn)的裝飾器是不是天衣無縫了?我們在調(diào)用的時候完全不用關(guān)心原函數(shù)擴(kuò)展了啥功能,而且也不用改變原來的調(diào)用方式,但是還是有一點區(qū)別的,比如函數(shù)自帶的一些方法 name、doc 還是有區(qū)別的,我們既然要偽裝了,就一定要力爭完美,這里可以使用 functools 的 wraps 方法
import time from functools import wrapsdef outter(func):@wraps(func)def wrapper(*args,**kwargs):start = time.time()res = func(*args,**kwargs)stop = time.time()print('function run time: %s'%(stop - start))return resreturn wrapper@outter def add(n):sum = 0for i in range(n):sum += ireturn sum@outter def index(x,y):print('from index %s:%s'%(x,y))無參裝飾器講完了,后續(xù)有機(jī)會講一下有參裝飾器以及疊加裝飾器的用法~
總結(jié)
以上是生活随笔為你收集整理的python函数装饰器一篇入魂的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 视频数字水印总结
- 下一篇: 自学python从字符串开始吧