每日最新頭條.有趣資訊

作為程式員,起碼要知道的 Python 修飾器!

Python修飾器是個非常強大的概念,可以用一個函數去“包裝”另一個函數。修飾器的思想,就是把函數中除了正常行為之外的部分抽象出去。這樣有很多好處,如很容易進行代碼複用,並且能遵守科裡定律(即一次隻做一件事)。

學習怎樣編寫修飾器,可以大幅度增加代碼的可讀性。它能改變函數的行為,而無需實際去改變函數的代碼(如添加日誌行等)。

而修飾器是Python中非常常用的工具,用過Flask、Click等框架的人,都應該很熟悉修飾器,但許多人只知道怎麽用,卻不知道該怎麽寫。如果你也不知道怎麽寫,那這篇文章,正是為你準備的!

修飾器的原理

首先我們來看一個Python修飾器的例子。下面是個非常簡單的例子,演示了修飾器的使用方法。

在Python中定義的函數實際上是個對象。

上面的函數Hello是個函數對象。@my_decorator實際上也是個函數,它接受hello對象,並返回另一個對象給解釋器。修飾器返回的對象會成為實際的hello函數。

本質上這跟正常的函數一樣,如hello = decorate(hello)。我們傳給函數decorate一個函數,decorate可以隨便怎樣去用這個函數,然後返回另一個對象。修飾器可以乾脆吞掉那個函數,或者如果需要,還可以返回某個不是函數的東西。

編寫自己的修飾器

前面說過,修飾器就是個簡單的函數,它接受函數作為輸入,返回一個對象。因此,編寫修飾器實際上只需要定義一個函數。

defmy_decorator(f):

return5

任何函數都可以用作修飾器。這個例子中,修飾器被傳入一個函數,然後返回一個不同的東西。它完全吃掉了輸入的函數,並且永遠返回5。

@my_decorator

defhello():

print('hello')

>>> hello()

Traceback (most recent call last):

File"", line1,in

TypeError:'int'object isnotcallable

'int'object isnotcallable

由於修飾器返回的是INT,INT不是Callable,因此不能作為函數調用。別忘了,修飾器的返回值會替換掉Hello。

>>> hello

5

絕大多數情況下,我們希望修飾器返回的對象能夠模擬被修飾的函數。這就是說,修飾器返回的對象應該也是個函數。

例如,假設我們希望在每次函數被調用時輸出一行文字,我們可以寫個函數輸出資訊,然後再調用輸入的函數。但這個函數必須由修飾器返回。因此我們的函數得寫成嵌套的形式,如:

defmydecorator(f):# f is the function passed to us from python

deflog_f_as_called():

print(f'was called.')

f()

returnlog_f_as_called

從上面的代碼可以看出,我們定義了個嵌套的函數,該嵌套函數被修飾器返回。這樣,Hello函數依然可以被當做函數調用,調用者並不知道Hello被修飾過了。現在Hello函數可以這樣定義:

@mydecorator

defhello():

print('hello')

輸出如下:

>>> hello()

was called.

hello

(注意:中的數字代表記憶體地址,因此每個人的都會不一樣。)

正確包裝函數

如果有必要,函數可以被修飾多次。這種情況下,修飾器會引起連鎖反應。本質上,每個修飾器的返回值都會傳遞給上一層的修飾器,直到最頂層。例如,下面的代碼:

@a

@b

@c

defhello():

print('hello')

解釋器實際上執行的是hello = a(b(c(hello))),所有修飾器都會互相包裝。你可以用之前定義的修飾器測試這一點,使用兩次就好:

@mydecorator

@mydecorator

defhello():

print('hello')

>>> hello()

.a at0x7f277383d378> was called.

was called.

hello

你會注意到,第一個修飾器包裝了第二個,然後產生了不同的輸出。

有意思的是,第一行輸出的結果是.a at 0x7f277383d378>,而不是像第二行那樣輸出我們期待的資訊:。

這是因為修飾器返回的是個新函數,這個新函數不叫Hello。作為例子來說這無所謂,但實際上這可能會讓測試失敗,或者讓試圖自省函數屬性的過程失敗。

所以,如果修飾器的思想是模擬被修飾的函數的行為,那麽它也應該模擬被修飾函數的樣子。幸運的是,有個Python標準庫functools模塊提供的修飾器wraps能做到這一點:

importfunctools

defmydecorator(f):

@functools.wraps(f)# we tell wraps that the function we are wrapping is f

deflog_f_as_called():

print(f'was called.')

f()

returnlog_f_as_called

@mydecorator

@mydecorator

defhello():

print('hello')

>>> hello()

was called.

was called.

hello

現在,新的函數看起來跟它修飾的函數一模一樣。但是,我們這個修飾器依然只能修飾不返回任何值,並且不接受任何輸入的函數。如果想讓它更通用,就必須負責傳遞函數參數,並且返回同樣的值。可以這樣修改:

importfunctools

defmydecorator(f):

@functools.wraps(f)# wraps is a decorator that tells our function to act like f

deflog_f_as_called(*args, **kwargs):

print(f'was called with arguments=and kwargs=')

value = f(*args, **kwargs)

print(f'return value')

returnvalue

returnlog_f_as_called

現在每次調用都會產生輸出,包含函數接收到的所有輸入,以及函數的返回值。現在可以用它來修飾任意函數,獲得關於函數的輸入和輸出的調試資訊,而用不著手動編寫日誌代碼了。

給修飾器增加變量

如果你寫的修飾器不是隻給自己用,而是想在產品代碼裡使用,那你可能需要把所有print語句換成日誌輸出語句。那樣的話就需要定義日誌的級別。

都定義成DEBUG級別也許沒問題,但還是能根據函數選擇級別最好。我們可以給修飾器提供變量,以改變修飾器的行為。例如:

@debug(level='info')

defhello():

print('hello')

上面的代碼可以指定,被修飾的函數應該以info級別輸出日誌,而不是DEBUG級別。這個功能的實現方法是寫個函數,返回修飾器。

沒錯,修飾器也是個函數。所以這段代碼實質上是hello = debug('info')(hello)。兩對括號看起來很奇怪,不過本質上說,DEBUG是個返回函數的函數。因此,修改我們之前的修飾器,我們還需要一層嵌套,這樣代碼如下所示:

import functools

defdebug(level):

defmydecorator(f)

@functools.wraps(f)

deflog_f_as_called(*args, **kwargs):

logger.log(level, f' was called with arguments= and kwargs=')

value = f(*args, **kwargs)

logger.log(level, f' return value ')

returnvalue

returnlog_f_as_called

returnmydecorator

上面的修改將DEBUG變成了一個返回修飾器的函數,返回的修飾器會使用正確的日誌級別。這段代碼看起來不太好看,而且嵌套太多了。

有個很酷的小技巧我非常喜歡,就是給DEBUG添加默認的level參數,返回一個部分函數。部分函數是“不完整的函數調用”,它包含一個函數和一些參數,這樣部分函數可以作為一個整體來傳遞,而無需調用實際的函數。

importfunctools

defdebug(f=None, *, level='debug'):

iffisNone:

returnfunctools.partial(debug, level=level)

@functools.wraps(f)# we tell wraps that the function we are wrapping is f

deflog_f_as_called(*args, **kwargs):

logger.log(level,f'was called with arguments=and kwargs=')

value = f(*args, **kwargs)

logger.log(level,f'return value')

returnvalue

returnlog_f_as_called

現在修飾器可以正常工作了:

@debug

defhello():

print('hello')

這樣就會使用DEBUG級別。或者可以覆蓋log級別:

@debug('warning')

defhello():

print('hello')

原文:https://timber.io/blog/decorators-in-python/

作者:Nick Humrich

譯者:彎月,責編:胡巍巍

獲得更多的PTT最新消息
按讚加入粉絲團