若是想在一個函數執行先後執行一些別的代碼,好比打印一點日誌用來輸出這個函數的調用狀況那應該怎麼作呢?python
#!/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語法的一種特殊改變,它容許咱們更方便地修改函數,方法以及類。app
當咱們按照如下方式編寫代碼時:less
@logger def add(x, y): ...
和單獨執行下面的步驟是同樣的:ssh
def add(x, y): ... logger_add = logger(add)
裝飾器內部的代碼通常會建立一個新的函數,利用*args
和**kwargs
來接受任意的參數,上述代碼中的wrap()函數就是這樣的。在這個函數內部,咱們須要調用原來的輸入函數(即被包裝的函數,它是裝飾器的輸入參數)並返回它的結果。可是也能夠添加任何想要添加的代碼,好比在上述代碼中輸出函數的調用狀況,也能夠添加計時處理等等。這個新建立的wrap函數會做爲裝飾器的結果返回,取代了原來的函數。ide
因此在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函數計時:ui
@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。也就是說通過裝飾器裝飾事後的函數的元信息發生了改變,這時候若是程序須要函數的元信息,那麼就有問題了。
以__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函數的過程稱爲函數的柯里化。
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高級軟件工程師、四年工做經驗,拒絕鹹魚爭當龍頭的斜槓程序員。
聽我說,進步多,程序人生一把梭
若是有幸能幫到你,請幫我點個【贊】,給個關注,若是能順帶評論給個鼓勵,將不勝感激。
職場亮哥文章列表:更多文章
本人全部文章、回答都與版權保護平臺有合做,著做權歸職場亮哥全部,未經受權,轉載必究!