Python 裝飾器是一個強大的概念,允許我們使用一個函數 「 包裝 」 另一個函數
除了正常的職責之外,裝飾器的另類使用想法是抽象出你想要一個功能或類做的東西,這可能有很多原因,例如 代碼重用 和堅持 科裏原則
通過學習如何編寫自己的裝飾器,我們可以顯着提高自己代碼的可讀性,因爲它們可以更改函數的行爲方式,而無需實際更改代碼 ( 例如添加日誌記錄行 )
它們是 Python 中相當常用的工具,對於使用諸如 flask
或click
之類的框架的人來說很熟悉
雖然很多人只知道如何使用它們,而不知道如何編寫自己的裝飾器
這篇文章是由朋友 @Timber 帶給我們的客串,如果你有興趣爲我們寫作,請隨時在 Twitter 上與我們聯繫
它 ( 裝飾器 ) 怎樣工作 ?
首先,讓我們在 Python
中展示一個裝飾器的例子,這是一個非常基本的裝飾器的例子
@my_decorator def hello(): print('hello')
當我們在 Python
中定義函數時,該函數將成爲一個對象,也就是說,Python 中,任何函數都是一個對象,可調用的對象
上面的函數 hello
是一個函數對象,@my_decorator
實際上是一個能夠使用 hello
對象並將另一個對象返回給解釋器的函數
裝飾器返回的對象就是所謂的 hello
。從本質上講,它就像你要編寫自己的普通函數一樣,例如 hello = decorate ( hello )
裝飾可以接收一個函數作爲參數 - 它可以使用任何它想要的 - 然後返回另一個對象
如果需要,裝飾器可以吞下函數 ( 也就是不返回該函數 ) ,或返回不是函數的函數
編寫自己的裝飾器
如上所述,裝飾器只是一個傳遞函數的函數,並返回一個對象
所以,要開始編寫裝飾器,我們只需要定義一個函數
def my_decorator(f): return 5
任何函數都可以用作裝飾器。在這個例子中,裝飾器接收一個函數,並返回一個不同的對象。它只是完全吞下傳遞給它的函數,並且總是返回 5
@my_decorator def hello(): print('hello')
>>> hello() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not callable 'int' object is not callable
因爲我們的裝飾器返回一個 int
而不是一個可調用的,所以它不能作爲函數調用
請記住,裝飾器的返回值替換了 hello
>>> hello 5
在大多數情況下,我們希望裝飾器返回的對象實際上模仿我們裝飾的函數。這意味着裝飾器返回的對象本身需要是一個函數
例如,假設我們只想在每次調用函數時打印,我們可以編寫一個打印該信息的函數,然後調用該函數。但是該函數需要由裝飾器返回
這通常會導致函數嵌套,例如
def mydecorator(f): # f is the function passed to us from python def log_f_as_called(): print(f'{f} was called.') f() return log_f_as_called
正如你所見,我們定義了一個嵌套的函數,而裝飾器函數則返回剛剛定義的嵌套函數。這樣,函數 hello
仍然可以像標準函數一樣被調用,調用者不需要知道它是否被裝飾
我們現在可以將 hello
定義如下
@mydecorator def hello(): print('hello')
我們會得到如下的輸出
>>> hello() <function hello at 0x7f27738d7510> was called. hello
注意:<function hello at 0x7f27738d7510>
引用內的數字對每個運行來說都不同,它代表內存地址
正確包裝函數
如果需要,可以多次裝飾一個函數。這種情況下,裝飾器會產生鏈式效應。基本上,頂部裝飾器從前者傳遞對象,依此類推。例如,如果我們有以下代碼
@a @b @c def hello(): print('hello')
解釋器本質上是執行 hello = a(b(c(hello)))
並且所有裝飾器將相互包裝
您可以使用我們現有的裝飾器自己測試,並使用它兩次
@mydecorator @mydecorator def hello(): print('hello') >>> hello() <function mydec.<locals>.a at 0x7f277383d378> was called. <function hello at 0x7f2772f78ae8> was called. hello
您將注意到第一個裝飾器,包裹第二個裝飾器,並單獨打印
你可能注意到這裏的一個有趣的事情是,第一行打印了 <function mydec.<locals>.a at 0x7f277383d378>
而不是第二行打印,而我們所期待的是:<function hello at 0x7f2772f78ae8>.
這是因爲裝飾器返回的對象是一個新函數,而不是 hello
。這對於我們這個簡單的例子來說很好,但是經常會破壞可能試圖反省函數屬性的測試和事情
如果你的想法是裝飾器像它裝飾的函數一樣,它還需要模仿該函數。幸運的是,Python 標準庫 functools
模塊中有一個名爲 wraps
的裝飾器
import functools def mydecorator(f): @functools.wraps(f) # we tell wraps that the function we are wrapping is f def log_f_as_called(): print(f'{f} was called.') f() return log_f_as_called @mydecorator @mydecorator def hello(): print('hello') >>> hello() <function hello at 0x7f27737c7950> was called. <function hello at 0x7f27737c7f28> was called. hello
現在,我們的新函數就像它的包裝/裝飾一樣。但是,我們仍然依賴於它什麼都不返回,並且不接受任何輸入的事實
如果我們想要更通用,我們需要傳入參數並返回相同的值。我們可以修改我們的函數讓它看起來像這樣
import functools def mydecorator(f): @functools.wraps(f) # wraps is a decorator that tells our function to act like f def log_f_as_called(*args, **kwargs): print(f'{f} was called with arguments={args} and kwargs={kwargs}') value = f(*args, **kwargs) print(f'{f} return value {value}') return value return log_f_as_called
現在我們每次調用函數時都會打印,包括函數接收的所有輸入以及返回的內容。現在,你可以簡單地裝飾任何現有函數,並在其所有輸入和輸出上進行調試日誌記錄,而無需手動編寫日誌記錄代碼
給裝飾器添加變量
如果我們使用裝飾器來處理我們想要發佈的任何代碼,而不僅僅是本地代碼,那麼可能希望用 logging
語句替換所有 print
語句。這種情況下,我們需要定義日誌級別。假設我們默認使用 debug
日誌級別,但這也可能取決於函數
我們可以爲裝飾器本身提供變量,以定義它應該如何表現。例如
@debug(level='info') def hello(): print('hello')
上面的代碼將允許我們指定此特定函數應該在 info
級別而不是 debug
級別進行日誌記錄。這在 Python
中中是通過編寫一個返回裝飾器的函數實現的
是的,裝飾者也是一個函數。所以這基本上是說 hello = debug('info')(hello)
。這個雙括號可能看起來很時髦,但基本上,debug
是函數,它返回一個函數
爲了將它添加到我們現有的裝飾器中,我們需要再嵌套一次,現在使我們的代碼看起來如下所示
import functools def debug(level): def mydecorator(f) @functools.wraps(f) def log_f_as_called(*args, **kwargs): logger.log(level, f'{f} was called with arguments={args} and kwargs={kwargs}') value = f(*args, **kwargs) logger.log(level, f'{f} return value {value}') return value return log_f_as_called return mydecorator
上面的更改將 debug
變爲一個函數,該函數返回一個使用正確日誌記錄級別的裝飾器,這變得有點難看,並且過度嵌套
我想做一些小技巧來解決這個問題,就是添加一個 kwarg 參數 level
並且默認只爲 debug
並返回一個 partial
partial
是一個 「 非完整函數調用 」,它包含一個函數和一些參數,因此它們作爲一個對象傳遞而不實際調用該函數
import functools def debug(f=None, *, level='debug'): if f is None: return functools.partial(debug, level=level) @functools.wraps(f) # we tell wraps that the function we are wrapping is f def log_f_as_called(*args, **kwargs): logger.log(level, f'{f} was called with arguments={args} and kwargs={kwargs}') value = f(*args, **kwargs) logger.log(level, f'{f} return value {value}') return value return log_f_as_called
現在裝飾器可以正常工作
@debug def hello(): print('hello')
然後就可以使用 debug
級別記錄日誌,或者,覆蓋日誌級別
@debug('warning') def hello(): print('hello')