引入裝飾器
如果想在一個函式執行前后執行一些別的代碼,比如列印一點日志用來輸出這個函式的呼叫情況那應該怎么做呢?
#!/usr/bin/env python
# coding=utf-8
def logger(fn): # 函式作為引數即fn可以為任何引數
def wrap(*args, **kwargs): # 可變引數args和kwargs
print('call {}'.format(fn.__name__))
ret = fn(*args, **kwargs) # 函式呼叫時的引數解構
print('{} called'.format(fn.__name__))
return ret # 回傳函式的回傳值
return wrap
def add(x, y):
return x + y
logger_add = logger(add)
print(logger_add.__name__)
print(logger_add)
ret = logger_add(3, 5)
print(ret)
#輸出結果:
wrap
<function logger.<locals>.wrap at 0x7fba35f4fe18>
call add
add called
8
也可以用以下方式來實作這種效果
@logger
def add(x, y):
return x + y ret = add(3, 5)
print(ret)
# 輸出結果:
call add
add called
8
這就是Python裝飾器的一個簡單使用
什么是裝飾器?
裝飾器是用于軟體設計模式的名稱, 裝飾器可以動態地改變函式,方法或類的功能,而不必直接使用子類或改變被裝飾的函式的源代碼,Python裝飾器是對Python語法的一種特殊改變,它允許我們更方便地修改函式,方法以及類,
當我們按照以下方式撰寫代碼時:
@logger
def add(x, y):
...
和單獨執行下面的步驟是一樣的:
def add(x, y):
...
logger_add = logger(add)
裝飾器內部的代碼一般會創建一個新的函式,利用*args和**kwargs來接受任意的引數,上述代碼中的wrap()函式就是這樣的,在這個函式內部,我們需要呼叫原來的輸入函式(即被包裝的函式,它是裝飾器的輸入引數)并回傳它的結果,但是也可以添加任何想要添加的代碼,比如在上述代碼中輸出函式的呼叫情況,也可以添加計時處理等等,這個新創建的wrap函式會作為裝飾器的結果回傳,取代了原來的函式,
所以在Python中,裝飾器的引數是一個函式, 回傳值是一個函式的函式,
裝飾器的示例:計時處理
寫一個裝飾器,用來計算一個函式的執行時間
import time
def timethis(fn):
def wrap(*args, **kwargs):
start = time.time()
ret = fn(*args, **kwargs)
end = time.time()
print(fn.__name__, end - start)
return ret
return wrap
如果要對add函式計時:
@timethis
def add(x, y):
return x + y
ret = add(3, 5)
print(ret)
# 輸出結果
add 1.9073486328125e-06
8
如果要對sleep函式計時:
@timethis
def sleep(x):
time.sleep(x)
sleep(3)
# 輸出結果
sleep 3.003262519836426
保存被裝飾函式的元資訊
什么是函式的元資訊
比如裝飾器的名稱,裝飾器的doc等等,我們可以使用dir函式列出函式的所有元資訊:dir(sleep),輸出結果如下
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
可以看到有很多的元資訊,我們比較常用的是__name__和__doc__這兩個屬性\
而且__doc__屬性也就是函式的檔案資訊,可以通過help函式查看得到
為什么要保存被裝飾函式的元資訊
改寫裝飾器的應用1:計時處理中的sleep函式如下:
@timeit
def sleep(x):
'''This function is sleep.'''
time.sleep(x)
sleep(3)
print(sleep.__name__)
print(sleep.__doc__)
以上代碼輸出結果如下:
3.0032713413238525
wrap
None
可以發現sleep函式的__name__是wrap,而不是sleep,而__doc__屬性為空,而不是sleep函式的docstring,也就是說經過裝飾器裝飾過后的函式的元資訊發生了改變,這時候如果程式需要函式的元資訊,那么就有問題了,
如何保存被裝飾函式的元資訊
方案1:手動給被裝飾函式的元資訊賦值
以__name__和__doc__這兩個屬性為例
import time
def timeit(fn):
def wrap(*args, **kwargs):
start = time.time()
ret = fn(*args, **kwargs)
end = time.time()
print(end - start)
return ret
wrap.__doc__ = fn.__doc__ # 手動賦值__doc__資訊
wrap.__name__ = fn.__name__ # 手動賦值__name__資訊
return wrap
@timeit
def sleep(x):
'''This function is sleep.'''
time.sleep(x)
if __name__ == "__main__":
sleep(3)
# print(dir(sleep))
print(sleep.__name__)
print(sleep.__doc__)
輸出結果如下
3.004547119140625
sleep
This function is sleep.
可以發現,__name__和__doc__這兩個屬性確實賦值成功了,
我們可以將元資訊賦值的程序改寫為函式,如下
import time
def copy_properties(src, dst): # 將元資訊賦值的程序改成函式copy_properties
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
def timeit(fn):
def wrap(*args, **kwargs):
start = time.time()
ret = fn(*args, **kwargs)
end = time.time()
print(end - start)
return ret
copy_properties(fn, wrap) # 呼叫copy_properties函式修改元資訊
return wrap
@timeit
def sleep(x):
'''This function is sleep.'''
time.sleep(x)
if __name__ == "__main__":
sleep(3)
# print(dir(sleep))
print(sleep.__name__)
print(sleep.__doc__)
這樣修改后,同樣可以解決問題,
繼續修改copy_properties函式,使得copy_properties可以回傳一個函式
def copy_properties(src):
def _copy(dst): # 內置一個_copy函式便于回傳
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
return _copy
def timeit(fn):
def wrap(*args, **kwargs):
start = time.time()
ret = fn(*args, **kwargs)
end = time.time()
print(end - start)
return ret
copy_properties(fn)(wrap) # 呼叫copy_properties函式
return wrap
同樣可以問題,
如果繼續修改copy_properties函式,使得_copy函式是一個裝飾器,傳入dst,回傳dst,修改如下:
def copy_properties(src): # 先固定dst,傳入src
def _copy(dst): # 傳入dst
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
return dst # 回傳dst
return _copy # 回傳一個裝飾器
def timeit(fn):
@copy_properties(fn) # 帶引數裝飾器的使用方法
def wrap(*args, **kwargs):
start = time.time()
ret = fn(*args, **kwargs)
end = time.time()
print(end - start)
return ret
return wrap
copy_properties在此處回傳一個帶引數的裝飾器,因此可以直接按照裝飾器的使用方法來裝飾wrap函式,這個修改copy_properties函式的程序稱為函式的柯里化,
方案2:使用functools庫的@wraps裝飾器
functools庫的@wraps裝飾器本質上就是copy_properties函式的高級版本:包含更多的函式元資訊,首先查看wrap裝飾器的幫助資訊:
import functools
help(functools.wraps)
wrap裝飾器函式的原型是:
wraps(wrapped, assigned=('module', 'name', 'qualname', 'doc', 'annotations'), updated=('dict',))
所以這個裝飾器會復制module等元資訊,但是也不是所有的元資訊,并且會更新dict,
使用示例如下:
import time
import functools
def timeit(fn):
@functools.wraps(fn) # wraps裝飾器的使用
def wrap(*args, **kwargs):
start = time.time()
ret = fn(*args, **kwargs)
end = time.time()
print(end - start)
return ret
return wrap
def sleep(x):
time.sleep(x)
print(sleep.__name__)
print(sleep.__doc__)
撰寫一個帶引數的裝飾器
如果上述的timeit裝飾器,我們需要輸出執行時間超過若干秒(比如一秒)的函式的名稱和執行時間,那么就需要給裝飾器傳入一個引數s,表示傳入的時間間隔,默認為1s,
我們可以給寫好的裝飾器外面包一個函式timeitS,時間間隔s作為這個函式的引數傳入,并且對內層的函式可見,然后這個函式回傳寫好的裝飾器,
import time
import functools
def timeitS(s):
def timeit(fn):
@functools.wraps(fn)
def wrap(*args, **kwargs):
start = time.time()
ret = fn(*args, **kwargs)
end = time.time()
if end - start > s:
print('call {} takes {}s'.format(fn.__name__, end - start))
else:
print('call {} takes {}s less than {}'.format(fn.__name__, end - start, s))
return ret
return wrap
return timeit
@timeitS(2)
def sleep(x):
time.sleep(x)
sleep(3)
sleep(1)
輸出結果如下:
call sleep takes 3.001342535018921s
call sleep takes 1.000471830368042s less than 2
所以,我們可以將帶引數的裝飾器理解為:
- 帶引數的裝飾器就是一個函式, 這個函式回傳一個不帶引數的裝飾器
記得幫我點贊哦!
精心整理了計算機各個方向的從入門、進階、實戰的視頻課程和電子書,按照目錄合理分類,總能找到你需要的學習資料,還在等什么?快去關注下載吧!!!

念念不忘,必有回響,小伙伴們幫我點個贊吧,非常感謝,
我是職場亮哥,YY高級軟體工程師、四年作業經驗,拒絕咸魚爭當龍頭的斜杠程式員,
聽我說,進步多,程式人生一把梭
如果有幸能幫到你,請幫我點個【贊】,給個關注,如果能順帶評論給個鼓勵,將不勝感激,
職場亮哥文章串列:更多文章

本人所有文章、回答都與著作權保護平臺有合作,著作權歸職場亮哥所有,未經授權,轉載必究!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/143229.html
標籤:Python
