橡皮擦镀虐,一個(gè)逗趣的互聯(lián)網(wǎng)高級(jí)網(wǎng)蟲箱蟆,新的系列沟绪,讓我們一起
Be More Pythonic
。
已完成的文章清單
- 滾雪球?qū)W Python 第二輪開啟空猜,進(jìn)階之路绽慈,列表與元組那些事兒
- 說完列表說字典,說完字典說集合辈毯,滾雪球?qū)W Python
- 關(guān)于 Python 中的字符串坝疼,我在補(bǔ)充兩點(diǎn),滾雪球?qū)W Python
- 列表推導(dǎo)式與字典推導(dǎo)式谆沃,滾雪球?qū)W Python
- 滾雪球?qū)W Python 之 lambda 表達(dá)式
- 滾雪球?qū)W Python 之內(nèi)置函數(shù):filter钝凶、map、reduce唁影、zip耕陷、enumerate
七、函數(shù)裝飾器
裝飾器(Decorators)在 Python 中据沈,主要作用是修改函數(shù)的功能哟沫,而且修改前提是不變動(dòng)原函數(shù)代碼,裝飾器會(huì)返回一個(gè)函數(shù)對(duì)象锌介,所以有的地方會(huì)把裝飾器叫做 “函數(shù)的函數(shù)”嗜诀。
還存在一種設(shè)計(jì)模式叫做 “裝飾器模式”,這個(gè)后續(xù)的課程會(huì)有所涉及孔祸。
裝飾器調(diào)用的時(shí)候隆敢,使用 @
,它是 Python 提供的一種編程語(yǔ)法糖崔慧,使用了之后會(huì)讓你的代碼看起來更加 Pythonic
拂蝎。
7.1 裝飾器基本使用
在學(xué)習(xí)裝飾器的時(shí)候,最常見的一個(gè)案例尊浪,就是統(tǒng)計(jì)某個(gè)函數(shù)的運(yùn)行時(shí)間匣屡,接下來就為你分享一下封救。
計(jì)算函數(shù)運(yùn)行時(shí)間:
import time
def fun():
i = 0
while i < 1000:
i += 1
def fun1():
i = 0
while i < 10000:
i += 1
s_time = time.perf_counter()
fun()
e_time = time.perf_counter()
print(f"函數(shù){fun.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")
如果你希望給每個(gè)函授都加上調(diào)用時(shí)間,那工作量是巨大的捣作,你需要重復(fù)的修改函數(shù)內(nèi)部代碼誉结,或者修改函數(shù)調(diào)用位置的代碼。在這種需求下券躁,裝飾器語(yǔ)法出現(xiàn)了惩坑。
先看一下第一種修改方法,這種方法沒有增加裝飾器也拜,但是編寫了一個(gè)通用的函數(shù)以舒,利用 Python 中函數(shù)可以作為參數(shù)這一特性,完成了代碼的可復(fù)用性慢哈。
import time
def fun():
i = 0
while i < 1000:
i += 1
def fun1():
i = 0
while i < 10000:
i += 1
def go(fun):
s_time = time.perf_counter()
fun()
e_time = time.perf_counter()
print(f"函數(shù){fun.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")
if __name__ == "__main__":
go(fun1)
接下來這種技巧擴(kuò)展到 Python 中的裝飾器語(yǔ)法蔓钟,具體修改如下:
import time
def go(func):
# 這里的 wrapper 函數(shù)名可以為任意名稱
def wrapper():
s_time = time.perf_counter()
func()
e_time = time.perf_counter()
print(f"函數(shù){func.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")
return wrapper
@go
def func():
i = 0
while i < 1000:
i += 1
@go
def func1():
i = 0
while i < 10000:
i += 1
if __name__ == '__main__':
func()
在上述代碼中,注意看 go
函數(shù)部分卵贱,它的參數(shù) func
是一個(gè)函數(shù)滥沫,返回值是一個(gè)內(nèi)部函數(shù),執(zhí)行代碼之后相當(dāng)于給原函數(shù)注入了計(jì)算時(shí)間的代碼键俱。在代碼調(diào)用部分兰绣,你沒有做任何修改,函數(shù) func
就具備了更多的功能(計(jì)算運(yùn)行時(shí)間的功能)编振。
裝飾器函數(shù)成功拓展了原函數(shù)的功能缀辩,又不需要修改原函數(shù)代碼,這個(gè)案例學(xué)會(huì)之后踪央,你就已經(jīng)初步了解了裝飾器臀玄。
7.2 對(duì)帶參數(shù)的函數(shù)進(jìn)行裝飾
直接看代碼,了解如何對(duì)帶參數(shù)的函數(shù)進(jìn)行裝飾:
import time
def go(func):
def wrapper(x, y):
s_time = time.perf_counter()
func(x, y)
e_time = time.perf_counter()
print(f"函數(shù){func.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")
return wrapper
@go
def func(x, y):
i = 0
while i < 1000:
i += 1
print(f"x={x},y={y}")
if __name__ == '__main__':
func(33, 55)
如果你看著暈乎了杯瞻,我給你標(biāo)記一下參數(shù)的重點(diǎn)傳遞過程镐牺。
還有一種情況是裝飾器本身帶有參數(shù),例如下述代碼:
def log(text):
def decorator(func):
def wrapper(x):
print('%s %s():' % (text, func.__name__))
func(x)
return wrapper
return decorator
@log('執(zhí)行')
def my_fun(x):
print(f"我是 my_fun 函數(shù)魁莉,我的參數(shù) {x}")
my_fun(123)
上述代碼在編寫裝飾器函數(shù)的時(shí)候睬涧,在裝飾器函數(shù)外層又嵌套了一層函數(shù),最終代碼的運(yùn)行順序如下所示:
my_fun = log('執(zhí)行')(my_fun)
此時(shí)如果我們總結(jié)一下旗唁,就能得到結(jié)論了:使用帶有參數(shù)的裝飾器畦浓,是在裝飾器外面又包裹了一個(gè)函數(shù),使用該函數(shù)接收參數(shù)检疫,并且返回一個(gè)裝飾器函數(shù)讶请。
還有一點(diǎn)要注意的是裝飾器只能接收一個(gè)參數(shù),而且必須是函數(shù)類型。
7.3 多個(gè)裝飾器
先臨摹一下下述代碼夺溢,再進(jìn)行學(xué)習(xí)與研究论巍。
import time
def go(func):
def wrapper(x, y):
s_time = time.perf_counter()
func(x, y)
e_time = time.perf_counter()
print(f"函數(shù){func.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")
return wrapper
def gogo(func):
def wrapper(x, y):
print("我是第二個(gè)裝飾器")
return wrapper
@go
@gogo
def func(x, y):
i = 0
while i < 1000:
i += 1
print(f"x={x},y={y}")
if __name__ == '__main__':
func(33, 55)
代碼運(yùn)行之后,輸出結(jié)果為:
我是第二個(gè)裝飾器
函數(shù)wrapper運(yùn)行時(shí)間是:0.0034401339999999975
雖說多個(gè)裝飾器使用起來非常簡(jiǎn)單风响,但是問題也出現(xiàn)了嘉汰,print(f"x={x},y={y}")
這段代碼運(yùn)行結(jié)果丟失了,這里就涉及多個(gè)裝飾器執(zhí)行順序問題了状勤。
先解釋一下裝飾器的裝飾順序鞋怀。
import time
def d1(func):
def wrapper1():
print("裝飾器1開始裝飾")
func()
print("裝飾器1結(jié)束裝飾")
return wrapper1
def d2(func):
def wrapper2():
print("裝飾器2開始裝飾")
func()
print("裝飾器2結(jié)束裝飾")
return wrapper2
@d1
@d2
def func():
print("被裝飾的函數(shù)")
if __name__ == '__main__':
func()
上述代碼運(yùn)行的結(jié)果為:
裝飾器1開始裝飾
裝飾器2開始裝飾
被裝飾的函數(shù)
裝飾器2結(jié)束裝飾
裝飾器1結(jié)束裝飾
可以看到非常對(duì)稱的輸出,同時(shí)證明被裝飾的函數(shù)在最內(nèi)層持搜,轉(zhuǎn)換成函數(shù)調(diào)用的代碼如下:
d1(d2(func))
你在這部分需要注意的是密似,裝飾器的外函數(shù)和內(nèi)函數(shù)之間的語(yǔ)句,是沒有裝飾到目標(biāo)函數(shù)上的葫盼,而是在裝載裝飾器時(shí)的附加操作残腌。
在對(duì)函數(shù)進(jìn)行裝飾的時(shí)候,外函數(shù)與內(nèi)函數(shù)之間的代碼會(huì)被運(yùn)行剪返。
測(cè)試效果如下:
import time
def d1(func):
print("我是 d1 內(nèi)外函數(shù)之間的代碼")
def wrapper1():
print("裝飾器1開始裝飾")
func()
print("裝飾器1結(jié)束裝飾")
return wrapper1
def d2(func):
print("我是 d2 內(nèi)外函數(shù)之間的代碼")
def wrapper2():
print("裝飾器2開始裝飾")
func()
print("裝飾器2結(jié)束裝飾")
return wrapper2
@d1
@d2
def func():
print("被裝飾的函數(shù)")
運(yùn)行之后废累,你就能發(fā)現(xiàn)輸出結(jié)果如下:
我是 d2 內(nèi)外函數(shù)之間的代碼
我是 d1 內(nèi)外函數(shù)之間的代碼
d2
函數(shù)早于 d1
函數(shù)運(yùn)行邓梅。
接下來在回顧一下裝飾器的概念:
被裝飾的函數(shù)的名字會(huì)被當(dāng)作參數(shù)傳遞給裝飾函數(shù)脱盲。
裝飾函數(shù)執(zhí)行它自己內(nèi)部的代碼后,會(huì)將它的返回值賦值給被裝飾的函數(shù)日缨。
這樣看上文中的代碼運(yùn)行過程是這樣的钱反,d1(d2(func))
執(zhí)行 d2(func)
之后,原來的 func
這個(gè)函數(shù)名會(huì)指向 wrapper2
函數(shù)匣距,執(zhí)行 d1(wrapper2)
函數(shù)之后面哥,wrapper2
這個(gè)函數(shù)名又會(huì)指向 wrapper1
。因此最后的 func
被調(diào)用的時(shí)候毅待,相當(dāng)于代碼已經(jīng)切換成如下內(nèi)容了尚卫。
# 第一步
def wrapper2():
print("裝飾器2開始裝飾")
print("被裝飾的函數(shù)")
print("裝飾器2結(jié)束裝飾")
# 第二步
print("裝飾器1開始裝飾")
wrapper2()
print("裝飾器1結(jié)束裝飾")
# 第三步
def wrapper1():
print("裝飾器1開始裝飾")
print("裝飾器2開始裝飾")
print("被裝飾的函數(shù)")
print("裝飾器2結(jié)束裝飾")
print("裝飾器1結(jié)束裝飾")
上述第三步運(yùn)行之后的代碼,恰好與我們的代碼輸出一致尸红。
那現(xiàn)在再回到本小節(jié)一開始的案例吱涉,為何輸出數(shù)據(jù)丟失掉了。
import time
def go(func):
def wrapper(x, y):
s_time = time.perf_counter()
func(x, y)
e_time = time.perf_counter()
print(f"函數(shù){func.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")
return wrapper
def gogo(func):
def wrapper(x, y):
print("我是第二個(gè)裝飾器")
return wrapper
@go
@gogo
def func(x, y):
i = 0
while i < 1000:
i += 1
print(f"x={x},y={y}")
if __name__ == '__main__':
func(33, 55)
在執(zhí)行裝飾器代碼裝飾之后外里,調(diào)用 func(33,55)
已經(jīng)切換為 go(gogo(func))
怎爵,運(yùn)行 gogo(func)
代碼轉(zhuǎn)換為下述內(nèi)容:
def wrapper(x, y):
print("我是第二個(gè)裝飾器")
在運(yùn)行 go(wrapper)
,代碼轉(zhuǎn)換為:
s_time = time.perf_counter()
print("我是第二個(gè)裝飾器")
e_time = time.perf_counter()
print(f"函數(shù){func.__name__}運(yùn)行時(shí)間是:{e_time-s_time}")
此時(shí)盅蝗,你會(huì)發(fā)現(xiàn)參數(shù)在運(yùn)行過程被丟掉了鳖链。
7.4 functools.wraps
使用裝飾器可以大幅度提高代碼的復(fù)用性,但是缺點(diǎn)就是原函數(shù)的元信息丟失了墩莫,比如函數(shù)的 __doc__
芙委、__name__
:
# 裝飾器
def logged(func):
def logging(*args, **kwargs):
print(func.__name__)
print(func.__doc__)
func(*args, **kwargs)
return logging
# 函數(shù)
@logged
def f(x):
"""函數(shù)文檔逞敷,說明"""
return x * x
print(f.__name__) # 輸出 logging
print(f.__doc__) # 輸出 None
解決辦法非常簡(jiǎn)單,導(dǎo)入 from functools import wraps
灌侣,修改代碼為下述內(nèi)容:
from functools import wraps
# 裝飾器
def logged(func):
@wraps(func)
def logging(*args, **kwargs):
print(func.__name__)
print(func.__doc__)
func(*args, **kwargs)
return logging
# 函數(shù)
@logged
def f(x):
"""函數(shù)文檔兰粉,說明"""
return x * x
print(f.__name__) # 輸出 f
print(f.__doc__) # 輸出 函數(shù)文檔,說明
7.5 基于類的裝飾器
在實(shí)際編碼中 一般 “函數(shù)裝飾器” 最為常見顶瞳,“類裝飾器” 出現(xiàn)的頻率要少很多玖姑。
基于類的裝飾器與基于函數(shù)的基本用法一致,先看一段代碼:
class H1(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return '<h1>' + self.func(*args, **kwargs) + '</h1>'
@H1
def text(name):
return f'text {name}'
s = text('class')
print(s)
類 H1
有兩個(gè)方法:
-
__init__
:接收一個(gè)函數(shù)作為參數(shù)慨菱,就是待被裝飾的函數(shù)焰络; -
__call__
:讓類對(duì)象可以調(diào)用,類似函數(shù)調(diào)用符喝,觸發(fā)點(diǎn)是被裝飾的函數(shù)調(diào)用時(shí)觸發(fā)闪彼。
最后在附錄一篇寫的不錯(cuò)的 博客,可以去學(xué)習(xí)协饲。
在這里類裝飾器的細(xì)節(jié)就不在展開了畏腕,等到后面滾雪球相關(guān)項(xiàng)目實(shí)操環(huán)節(jié)再說。
裝飾器為類和類的裝飾器在細(xì)節(jié)上是不同的茉稠,上文提及的是裝飾器為類描馅,你可以在思考一下如何給類添加裝飾器。
7.6 內(nèi)置裝飾器
常見的內(nèi)置裝飾器有 @property
而线、@staticmethod
铭污、@classmethod
。該部分內(nèi)容在細(xì)化面向?qū)ο蟛糠诌M(jìn)行說明膀篮,本文只做簡(jiǎn)單的備注嘹狞。
7.6.1 @property
把類內(nèi)方法當(dāng)成屬性來使用,必須要有返回值誓竿,相當(dāng)于 getter
磅网,如果沒有定義 @func.setter
修飾方法,是只讀屬性筷屡。
7.6.2 @staticmethod
靜態(tài)方法涧偷,不需要表示自身對(duì)象的 self
和自身類的 cls
參數(shù),就跟使用函數(shù)一樣速蕊。
7.6.3 @classmethod
類方法嫂丙,不需要 self
參數(shù),但第一個(gè)參數(shù)需要是表示自身類的 cls
參數(shù)规哲。
7.7 這篇博客的總結(jié)
關(guān)于 Python 裝飾器跟啤,網(wǎng)上的文章實(shí)在太太多了,學(xué)習(xí)起來并不是很難,真正難的是恰到好處的應(yīng)用在項(xiàng)目中隅肥,希望本篇博客能對(duì)你理解裝飾器有所幫助竿奏。
其他內(nèi)容也可以查閱 官方手冊(cè)。
相關(guān)閱讀
今天是持續(xù)寫作的第 <font color="red">103</font> / 200 天秃症。
如果你想跟博主建立親密關(guān)系候址,可以關(guān)注同名公眾號(hào) <font color="red">夢(mèng)想橡皮擦</font>,近距離接觸一個(gè)逗趣的互聯(lián)網(wǎng)高級(jí)網(wǎng)蟲种柑。
博主 ID:夢(mèng)想橡皮擦岗仑,希望大家<font color="red">點(diǎn)贊</font>、<font color="red">評(píng)論</font>聚请、<font color="red">收藏</font>荠雕。