Advanced Python(1)——裝飾器(Decorator)
裝飾器,作為Python中的一個非常重要的功能,在web系統,日誌列印等領域中有著很廣泛的應用,比如Flask和Django框架的代理機制就是使用了裝飾器。這裡,我將總結《Expert Python Programming》第2版的裝飾器部分我認為重點的內容,並配合例子進行說明。
1、Python的裝飾器(Decorator)是什麼?
Python裝飾器,簡單來講,就是使函式包裝和類的方法包裝(一個函式,接受函式並返回其增強函式 (對原函式增加一些日誌資訊或更高階的處理的方式))變得容易閱讀和理解。其最初的使用場景是在方法定義的開頭,將一個類的方法定義為靜態方法/類方法——假設程式碼最後的staticmethod
classmethod
是預先定義好的:
class WithOutDecorators:
def static_method_A():
print("This is a static method A")
def class_method_B():
print("This is a class method B")
# 對此靜態方法進行封裝
static_method_A = staticmethod(static_method_A)
# 對類方法進行封裝
class_method_B = classmethod(class_method_B)
如果用裝飾器(decorator)寫的話,可以想到,對於有很多靜態方法和類方法的類來講,採取decorator機制會使得程式碼可讀性更強、容易理解的優點。
class WithDecorators:
@staticmethod
def static_method_A():
print("This is a static method A")
@classmethod
def class_method_B ():
print("This is a class method B")
2、裝飾器(Decorator)一般用法和可能實現
裝飾器(Decorator)通常是一個命名的物件——(注意,不允許使用lambda表示式!),在被裝飾函式呼叫時,接受單一引數,並返回另一個可呼叫的物件(callable object)。所以,任何可呼叫物件(類內部實現了__call__
方法的物件)都可以用作裝飾器。他們返回的物件也不是簡單的函式,也可能是實現了自己的__call__
方法的複雜類的例項(instance)。
事實上,任何函式都可以用作decorator,因為python沒有規定裝飾器的返回型別。因此可以基於此做一些有趣的實驗——比如將str()作為裝飾器,雖然會報錯,但是也是蠻有意思。
① 作為函式
最簡單也是最常用的用法,就是編寫一個函式作為Decorator,通用模式為:# 1 裝飾器函式 common used def mydecorator(func): def wrapped(*args, **kwargs): print("呼叫函式之前處理...") # 在函式呼叫之前做一些處理 result = func(*args, **kwargs) print('函式呼叫完成之後處理...') # 函式呼叫完成之後,做點什麼處理 return result return wrapped @mydecorator def calc(n): assert type(n) == int, "輸入務必是integer" number = n*2 print(number) return number calc(10) # 呼叫函式之前處理... # 20 # 函式呼叫完成之後處理...
② 作為類
雖然decorator總是幾乎可以用函式實現,但是某些情況下,使用使用者self-define的類可能會更好————比如需要複雜引數化或者依賴於特定狀態的decorator,那麼這種說法往往是對的。
通用模式如下:# 2 裝飾器作為類, 當decorator需要複雜的引數化或依賴於特定狀態的時候,需要這麼做 class DecoratorAsClass(object): def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): # 呼叫原始函式之前做些什麼 print("類裝飾器:呼叫函式之前處理...") result = self.func(*args, **kwargs) # 呼叫原始函式之後做些什麼 print("類裝飾器:呼叫函式之後處理...") return result @DecoratorAsClass def calc2(n): assert type(n) == int, "輸入務必是integer" number = n*10 print(number) return number calc2(50) # 類裝飾器:呼叫函式之前處理... # 500 # 類裝飾器:呼叫函式之後處理...
③ 引數化裝飾器
實際程式碼中,通常需要使用引數化的裝飾器。如果用函式作為裝飾器的話,那麼解決方式很簡單————需要在wrap一層(也就是從之前2層變成了3層):# 3 引數化裝飾器————我的理解就是在函式式裝飾器基礎上,再加一層 # 以一個重複列印的裝飾器為例進行說明 def repeat(number): """ 多次重複執行裝飾函式 重複次數為number的數目。 """ def actual_decorator(func): def wrapped(*args, **kwargs): result = None print("引數化裝飾器:呼叫函式之前處理...") # 在函式呼叫之前做一些處理 for i in range(number): result = func(*args, **kwargs) print('引數化裝飾器:函式呼叫完成之後處理...') # 函式呼叫完成之後,做點什麼處理 return result return wrapped return actual_decorator # 務必注意,及時引數化decorator有預設值,也必須加括號——————@repeat(), 而不可以使用@repeat這種形式 @repeat(3) def calc3(n): assert type(n) == int, "輸入務必是integer" print(n) return n calc3(100) #引數化裝飾器:呼叫函式之前處理... #100 #100 #100 #引數化裝飾器:函式呼叫完成之後處理...
- ④ 儲存內省的裝飾器(保留被封裝的原函式名稱和文件)
這塊沒細看,有興趣的同學可以看書學習。
3、裝飾器(Decorator)使用和有用例子
由於裝飾器在module被首次讀取的時候由直譯器(intepreter)來載入,所以其使用受限於通用的包裝器(wrapper),如果裝飾器與方法的類或所增強的函式簽名繫結,應該將其重構為常規的可呼叫物件,以避免複雜性。
常規的裝飾器模式如下:
- 引數檢查
- 快取
- 代理
- 上下文提供
(1)引數檢查
其目的是:檢查函式接受或者返回的引數,在特定的上下文中執行有用。書中的例子是:“當一個函式通過XML-RPC來呼叫,那麼Python無法像靜態語言C/C++一樣,直接提供其完整的簽名。當XML-RPC客戶端請求函式簽名時,就需要用這功能來提供內省能力。”
也就是說,在一些web應用中,採用Decorator機制的引數檢查會發揮作用,這塊我暫時沒看。
(2)快取
快取和引數檢查十分相似,不過,它重點關注的是不受狀態影響的函式,即唯一確定的引數一定會產生唯一確定的結果,這就是函數語言程式設計(functional programming)的風格。
因此,快取decorator可以將輸出與計算其所需要的引數放在一起,並在後續的呼叫中直接返回它。這種行為被稱為memoizing。
下面就是一個裝飾器的實現,其中
is_obsolete
函式是判斷某組(函式,引數)的計算結果是否超過cache的固定時間,如果超過就返回yes。
compute_key
的作用是計算某組(函式,引數)的簽名。
一個明顯的問題就是,cache到時了,字典中對應的結果沒被擦除,這塊可以根據自己的需要個性定製。有想更深入學習快取裝飾器的,建議看一下functools裡面的lru_cache,它也是以裝飾器的形式使用。網上也有比較詳實的說明介紹。
(3)代理(Web)
代理裝飾器使用全域性機制來標記和註冊函式。舉個例子,一個根據當前使用者來保護程式碼訪問的安全層可以使用集中式檢查器和相關的可呼叫物件要求的許可權來實現。
這種模型通常用於Python的Web框架中,用於定義可釋出類的安全性。例如,Django提供decorator來保護函式訪問的安全。
下面的簡單例子是,當前使用者被儲存在全域性變數(用到了python的全域性機制,globals()
)。在方法被訪問的時候decorator會檢查其角色,看是否符合。
對複雜情況,可以對每個角色的分工設定許可權,比如某個使用者只能看到網頁A,B,而root使用者能看到網頁A,B,C,D這種設定。
(4) 上下文提供者(通常被上下文管理器(with …)所替代)
上下文裝飾器確保函式可以執行在正確的上下文中,或者在函式前後執行一些程式碼。換句話說,它設定並復位一個特定的執行環境。
舉個例子,當一個數據需要在多個thread之間進行共享的時候,需要鎖來保護它被多次訪問(臨界區)。這個鎖可以在裝飾器中編寫,程式碼如下: