當前位置:
首頁 > 知識 > 作為程序員,起碼要知道的 Python 修飾器!

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

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


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

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

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

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

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

修飾器的原理

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

@my_decorator
def hello():
print("hello")

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

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

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

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

編寫自己的修飾器

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

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,INT不是Callable,因此不能作為函數調用。別忘了,修飾器的返回值會替換掉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被修飾過了。現在Hello函數可以這樣定義:

@mydecorator
def hello():
print("hello")

輸出如下:

>>> hello()
<function hello at 0x7f27738d7510> was called.
hello

(注意:<function hello at 0x7f27738d7510>中的數字代表內存地址,因此每個人的都會不一樣。)

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

正確包裝函數

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

@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

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

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

給修飾器增加變數

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

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

@debug(level="info")
def hello():
print("hello")

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

沒錯,修飾器也是個函數。所以這段代碼實質上是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變成了一個返回修飾器的函數,返回的修飾器會使用正確的日誌級別。這段代碼看起來不太好看,而且嵌套太多了。

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

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級別。或者可以覆蓋log級別:

@debug("warning")
def hello():
print("hello")

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

作者:Nick Humrich

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

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 CSDN 的精彩文章:

React Native 已死?
深度防範,如何應對區塊鏈安全問題?

TAG:CSDN |