更多Java面試資料(操做系統,網絡,zk,mq,redis,java等):github.com/yuhaqiang12…java
先說結論 java 註解能實現的功能,python 的裝飾器絕大部分都是能夠勝任的,裝飾器更像 Java 中註解加上Aop二者的組合python
python 是一門極簡的語言,語言簡潔學習起來也是至關輕鬆的,可是依然有一些高級技巧,例如裝飾器,協程,併發會讓人感受困惑,失望與沮喪,本文將重點講解 python裝飾器的使用,使用經常使用的例子讓咱們更直觀的看到裝飾器的強大表達能力,最後也給出了編寫裝飾器常見的工具。git
熟悉 java的同窗必定熟悉註解的使用,藉助於註解能夠定義元數據配置,咱們經常有這種感覺,"只要加上這個註解,個人組件就會被註冊進去","只要加上這個註解,就會添加事務控制",也會困惑,"爲何加了這個註解依然沒有生效?", python 沒有提供像Java似的註解,可是提供了相比註解表達能力更增強大的裝飾器。 例如 web框架 Flask 中的route ,errorhandler,及 python 自帶的 property,staticmethod等。 實際上java 註解能實現的功能,python 的裝飾器絕大部分都是能夠勝任的,裝飾器更像 Java 中註解加上Aop二者的組合, 這個結論最後咱們會重點討論,先按下不表。如今首先以日誌打印的簡單例子初步講解一下裝飾器的使用程序員
def log(func): #@1
def func_dec(*args, **kwargs): #@2
r = func(*args, **kwargs) #@3
print("didiyun execute done:%s" % func.__name__)
return r
return func_dec
@log #@4
def test_dec(size, length, ky=None):
print "didiyun execute test_dec param:%s, %s, %s" % (size, length, ky)
def test():
test_dec(ky="yuhaiqiang", length=3, size=1)
""
輸出結果能夠看到裝飾器的裝飾邏輯已正確被執行
didiyun execute test_dec param:1, 3, yuhaiqiang
didiyun execute done:test_dec
""
@1 定義 log 裝飾器,輸入參數func是須要被裝飾的函數,本例中輸出打印是 test_dec
@2 定義一個裝飾函數,參數類型包括變長的位置參數和名字參數,適應被裝飾函數不一樣的參數組合,這種寫法能夠表明任意參數組合
@3 執行實際的函數func_dec, 注意處理返回值,不要"吞掉"被裝飾函數的返回值
@4 在被裝飾函數上添加裝飾器,注意此處不要加(),後面會解釋具體緣由,瞭解該緣由,就能徹底瞭解裝飾器的小九九
複製代碼
裝飾器能夠在函數外層添加額外的功能裝飾原函數, 本例的裝飾器只是在函數外層打印一行日誌,實現的是很是簡單的功能,實際中裝飾器並非"僅僅打印日誌的雕蟲小技",還能實現其餘更有用的功能github
class Lock:
def __init__(self, filename, block=True):
#block 參數爲 true表明阻塞式獲取。 False爲非阻塞,若是獲取不到馬上返回 false
self.filename = filename
self.block = block
self.handle = open(filename, 'w')
def acquire(self):
if not self.block:
try:
fcntl.flock(self.handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
return True
except:
return False
else:
fcntl.flock(self.handle, fcntl.LOCK_EX)
return True
def release(self):
fcntl.flock(self.handle, fcntl.LOCK_UN)
self.handle.close()
複製代碼
藉助於 fcntl 庫 咱們已經實現文件鎖,感興趣的讀者能夠深刻了解一下 fcntl 庫,下面咱們以文件鎖爲例,介紹一下裝飾器很實用很常見的一些功能。web
def file_lock(lock_name, block=True): #@1
def wrapper(func):
def f(*args, **kwargs):
name = lock_name
lock = Lock(name, block)
acquire = lock.acquire()
if not acquire:
print("failed to acquire lock:%s,now ignore" % name)
return #@2
print("acquire process lock:%s" % name)
try:
return func(*args, **kwargs)
finally:
lock.release() #@3
print("release process lock:%s" % name)
return f
return wrapper
@file_lock(name="/var/local/file", block=True) #@4
def get_length():
pass
get_length()
輸出 #@5
acquire process lock:/var/local/file
execute test_dec param:1
release process lock:/var/local/file
複製代碼
使用文件鎖以後,調用該方法必須先獲取到鎖,不然只能先阻塞。 所以實際的處理方法不須要處理同步邏輯, 只須要一行裝飾器, 就額外擴展了同步功能, 經過異常控制,還能保證文件鎖必定能夠被釋放, 避免文件鎖泄露, 經過裝飾器咱們還能夠實現不少其餘有用的功能 ,可是文件鎖裝飾器的實現已經相比日誌裝飾器複雜了, 仔細觀察, 它已經 嵌套了 三層函數,後續咱們會優化這個問題。面試
在1.0 日誌裝飾器的例子咱們留下了一個疑問,爲何 log 裝飾器不須要加() ,而文件鎖裝飾器的使用卻加了()redis
回到1.0 的裝飾器實現,假如咱們不使用@ log 的方式,使用以下方式呢?能不能實現相同的邏輯?spring
@log
def foo():
pass
foo() # 至關於 log(foo)(),log(func) 返回裝飾函數,最後的括號表明執行
foo = log(foo) # 就是裝飾器語法糖幫咱們作的
複製代碼
log 方法接受的參數是 func , 天然當手動顯式 調用 log 裝飾 foo 函數時, 絲絕不影響實現裝飾的功能. 可是顯得咱們很囉嗦很蠢, 幸福的是 python 提供了裝飾器的語法糖, 使用該語法糖就好像咱們手動執行裝飾同樣。 可是若是咱們加上括號表明什麼意思呢? @log() 的寫法, 不就至關於調用了 log 函數,可是又不給其傳參? 實際 python 解釋器也是這麼"抗議" 咱們的。設計模式
可是爲何文件鎖又加上了括號呢?答案是,裝飾器有時候須要一些額外的參數,例如 Flask 中咱們經常使用的 route,咱們須要告訴 Flask, 如何 將url映射到具體的 handler, 天然須要告訴 route, 須要綁定的 url 是什麼, 和 spring 的@RequestMapping做用相似
當裝飾器加上參數以後, 驚訝的發現裝飾器更像是三層函數了....., 可理解性已經極差了, 可是一旦理解以後咱們會發現三層函數是緣由的
不妨這樣理解, 當裝飾器沒有參數, 就像log 裝飾器,該裝飾器接受參數爲 func, 咱們稱其爲"兩層"裝飾器 ,以上咱們已經分析了它的的原理, foo = log(foo). 裝飾器的@標記等於告訴 python解釋器, "你把@下一行的函數,做爲參數傳給該裝飾器,而後把返回值賦值給該函數", 至關於執行 foo = log(foo) 當咱們調用 foo() 至關因而調用 log(foo)()。
對於帶參數的裝飾器file_lock(name,block), 咱們分紅兩個階段理解,回顧一下 file_lock 的三層函數實現,咱們在第二層定義了一個 wrapper 函數,該函數接受了一個 func 參數,隨後咱們在 file_lock 的最後將其return, 咱們能夠這樣認爲
@file_lock(name="/var/local/test",block=True)
def test()
pass
wrapper = file_lock(name, block) #第一階段
test = wrapper(test) #第二階段
複製代碼
第一階段執行了最外層的函數file_lock, 返回了 wrapper。 第二階段,使用 wrapper 裝飾 test, 第二階段咱們已經熟悉理解了。 實際上, 只是第一階段是多執行的。 因爲咱們多給它加了一個括號, python 解釋器天然會去執行該函數, 該函數返回另外一個裝飾函數, 這樣就到了第二階段。
python 解釋器但願咱們這樣去理解,不然三層函數的寫法很讓人崩潰。後續咱們繼續探索裝飾器,能不能實現相同的功能,可是能擺脫編寫三層函數的噩夢。
以上分析了帶參數和不帶參數的裝飾器的區別,及如何在內心去理解與接受這種寫法, python 經過語法糖, 函數之上的裝飾器定義 代替蠢笨的手動裝飾調用。 咱們能夠實現複雜的裝飾器 但卻能提供極其 優雅的使用方式給調用方, 仍是 讓人鼓舞的,事實上, python的框架中大量的使用了裝飾器。也說明了裝飾器的強大與優雅。
python 是解釋執行的語言,咱們作一個小實驗,以上例子先定義 log 裝飾器,然後再使用 log裝飾器,若是置換一下順序
@log("say some thing")
def test_dec(size, length, ky=None):
print "execute test_dec param:%s, %s, %s" % (size, length, ky)
def log(info=None):
def wrapper(func):
def func_dec(*args, **kwargs):
r = func(*args, **kwargs)
print("execute done:%s" % func.__name__)
return r
return func_dec
return wrapper
複製代碼
毫無疑問,這樣會報語法錯誤, python是 從python文件從上到下執行解釋執行, 只有已經定義log, 才能使用它。 在python 中,函數是一等公民, 函數也是對象,在定義函數時,也就是在聲明一個函數對象
def foo():
pass
複製代碼
def foo()就是在聲明一個函數對象, foo 便是該函數對象的引用, 咱們能夠額外定義該對象的屬性, 經過dir(foo) 查看一下該函數對象有哪些屬性,實際上是和類實例對象沒有區別的
以上提過 test 被 log裝飾 後, test() 等同於log(test)() , python 裝飾器解釋執行完 @log def test(),等同於test=log(test) 此時 test引用的函數對象是 log裝飾後的函數對象
@log
def test():
pass
test=log(test)
複製代碼
實際開發中咱們常常會遇到使用多個裝飾器,若是讀者理解了3.0,及以上的函數對象的概念,其實應能能猜出來裝飾器的裝飾順序,天然是從上往下執行的 foo = a(b(c(foo))) 可是實際的代碼執行順序是 c->b->a
@a
@b
@c
def foo()
pass
複製代碼
以上咱們使用日誌裝飾器和文件鎖裝飾器介紹了裝飾器的使用,而且討論了帶參數及不帶參數裝飾器的區別。其中"三層函數"的定義方式可讀性很是差,在下一節將重點討論如何使用類實現裝飾器,簡化三層裝飾器的邏輯,減小類似代碼的編寫
在本節中,咱們重點優化三層裝飾器的編寫,除此以外,筆者在實際開發中還發現了其餘常見的需求, 例如
從以上三點出發,能夠看到裝飾器的邏輯有某些通用的部分,然而以上裝飾器的例子都是經過函數實現的, 但函數在內部狀態, 繼承等方面明顯不如類,因此咱們嘗試使用類實現裝飾器。並嘗試實現一個通用的裝飾基類
python 提供了不少奇異方法,所謂的奇異方法是指,只要你實現了這個方法,就可使用 python 的某些工具方法,例如實現__ len__ 方法,可使用 len() 獲取長度,實現__ iter__ 可使用 iter方法返回一個迭代器,其餘方法 還有 "__eq__","__ne__", "__next__", 等。 其中當實現__ call__ 方法時, 類能夠被當作一個函數使用,例如如下示例
class FuncClass(object):
def __call__(self):
print("didiyun")
>>>F = FuncClass()
>>>F()
didiyun
複製代碼
是否也可使用 python 的這個特性實現裝飾器呢?答案是能夠的,讓咱們來實現一個裝飾基類, 解決以上的痛點
class BaseDecorator(object):
def __call__(self, *_, **kwargs): #@1
return self.do_call(**kwargs) #@2
def do_call(self, *_, **decorator_kwargs):
def wrapper(func):
wrapper.__explained = False
@wraps(func) #@3
def _wrap(*args, **kwargs):
if not wrapper.__explained: #@4
self._add_dict(func, decorator_kwargs)
wrapper.__explained = True
return self.invoke(func, *args, **kwargs) #@5
self._add_dict(_wrap, decorator_kwargs)
_wrap = self.wrapper(_wrap) #@6
return _wrap
return wrapper
def wrapper(self, wrapper):
return wrapper
def _add_dict(self, func, decorator_kwargs):
for k, v in decorator_kwargs.items():
func.__dict__[k] = v
def invoke(self, func, *args, **kwargs):
return func(*args, **kwargs)
BaseDecorator實現的並非具體的某個裝飾器邏輯,它能夠做爲裝飾器類的基類,以上咱們曾分析編寫裝飾器通用的需求已經痛點。如下先具體講解這個類的實現,然後在討論如何使用
1. __call__ 函數簽名,*_ 表明忽略變長的位置參數,只接受命名參數。實際的裝飾器中,通常都是使用命名參數.代碼可讀性高
2. __call__ 自己的實現邏輯委託給了 do_call 方法,主要是考慮, BaseDecorator 做爲裝飾基類,須要提供某些工具方法及可擴展方法,可是__ call__ 方法自己沒法被繼承,因此咱們退而求次,將工具方法封裝在自定義方法中,子類仍是須要從新
實現__ call__, 並調用 do_call 方法, do_call 方法的簽名和__ call__ 相同
3. functools提供了 wraps 裝飾器, 以上咱們分析過python是使用裝飾後的函數對象替換以前的函數對象達到裝飾的效果, 可能有人會有疑問,若是 以前的函數對象有一些自定義屬性呢? 裝飾後的新函數會不會丟掉,答案是確定的, 咱們能夠訪問以前的函數對象,給其設置屬性,
這些屬性會被存儲在 對象的__ dict__ 字典中, 而wraps 裝飾器會把原函數的__ dict__拷貝到新的裝飾後的函數對象中, 所以 wraps 裝飾後,就不會丟掉原有的屬性, 而不使用則必定會丟掉。 感興趣的讀者能夠點開 wraps 裝飾器,看一下具體實現邏輯
4. 在本節開始,咱們提出裝飾器的通用需求,其中之一是須要將裝飾器的參數存放到被裝飾的函數中,_add_dict方法即是將裝飾器參數設置到原函數以及裝飾後的函數中
5. invoke 負責實現具體的裝飾邏輯,例如日誌裝飾器僅僅是打印日誌,那麼該方法實現就是打印日誌,以及調用原函數。 文件鎖裝飾器,則須要先獲取鎖後在執行原函數,具體的裝飾邏輯在該方法中實現, 具體的裝飾器子類應該重寫該方法。下一節咱們繼承該BaseDecorator重寫以上的日誌及文件鎖裝飾器
6. invoke 方法是裝飾函數調用時被觸發的, 而 wrapper 方法只會被觸發一次,當 python 解釋器執行到@log時,會執行該裝飾器的wrapper 方法。至關於,函數被定義的時候,執行了 wrapper方法,在該方法內能夠實現某些註冊功能。將函數和某些鍵值映射起來放到字典中,例如 web 框架的 url和handler映射
關係的註冊
複製代碼
BaseDecorator 抽出來了 invoke,wrapper 目的是讓子類裝飾器能夠在這兩個維度上擴展,分別實現裝飾,及某些註冊邏輯,在下一節咱們嘗試重寫日誌及文件鎖裝飾器,更直觀的感覺BaseDeceator 給咱們帶來的便利
class _log(BaseDec):
def invoke(self, func, *args, **kwargs): #@1
print("execute done:%s, %s" % (func.__name__,func.desc) ) #@2
return func(*args, **kwargs)
def __call__(self, desc):
return self.do_call(desc=desc)
log = _log() #@2
1. invoke方法中包括原函數以及原函數的輸入參數,該輸入參數不是裝飾器的參數信息
2. 經過 func 能夠訪問到裝飾器中定義的 desc 參數信息
3. 建立裝飾器實例, 即可以像以前同樣使用 @log,須要注意的是,該裝飾類變成單例, 在定義裝飾邏輯的時候,不要輕易在 self 中儲存變量
複製代碼
經過重寫日誌裝飾器, 能夠看到已經擺脫了三層函數的噩夢, 成功的分離了裝飾器的基本代碼,以及裝飾邏輯代碼,咱們能夠更加聚焦於裝飾邏輯的核心代碼編寫,同時能夠經過原函數訪問裝飾器中輸入的參數,例如能夠訪問到日誌裝飾器的 desc 如下咱們再重寫文件鎖裝飾器
class _file_lock(BaseDec):
def invoke(self, func, *args, **kwargs):
name = func.name #@1
lk = Lock(name, True)
acquire = lk.acquire()
if not acquire:
print("failed to acquire lock:%s,now ignore" % name)
return
print("acquire process lock:%s" % name)
try:
return func(*args, **kwargs)
finally:
lk.release()
print("release process lock:%s" % name)
def __call__(self, name, block=True):
return self.do_call(name=name, block=block) #@2
file_lock = _file_lock() #@3
1. 能夠經過 func 訪問到裝飾器中定義的 name 參數
2. 把參數傳給 do_call 委託執行
3. 建立文件鎖實例,其餘位置就可使用@file_lock了
複製代碼
使用新的裝飾基類後, 編寫新的裝飾器子類,是很是輕鬆方便的事情, 不須要再躡手躡腳的定義複雜的三層函數, 不須要重複的設置裝飾器參數, 若是咱們在項目中大量使用裝飾器, 不妨使用裝飾基類, 統一常見的功能需求。裝飾器的更多用法還須要讀者去發掘,可是熟悉 java 的同窗 必定熟悉 aop 的理念, 筆者深受 java 折磨多年, 對 aop也幾分偏心, 在我看來, python 的裝飾器是 java 中的註解加 aop 的結合。下一節咱們橫向對比一下 java 註解與 python 裝飾器的類似點, 論證文章開頭咱們留下的一個論點
之因此對比 java 註解,主要是筆者想從 java 的某些用法獲得某些借鑑與參考, 以便於咱們應用到 python 中,經過兩種語言的對比可讓咱們更深入的理解語言設計者添加該特性的初衷,以便更好的使用該特性。 更重要的是,讓咱們面對不一樣語言的異同 有更大的包容性, 站在欣賞的角度去對比思考,對於咱們快速掌握新的語言十分有益。本節毫不是 爲了爭吵兩種語言的優劣, 更不想挑起語言的戰爭
裝飾器和註解最直觀的類似點可能就是@艾特符號了, python 使用相同的符號 對於 java 程序員是一種"關照"。 由於 java 程序員對於註解有一種特殊的迷戀, 第三方框架就是使用眼花繚亂的註解 幫助 java 程序員實現一個個神奇的功能。而裝飾器也是能夠勝任的
java 的註解自己只是一種元數據配置,在沒有註解以前, 若是實現相同的元數據配置只能依賴於 xml 配置, 有了註解以後,咱們能夠把元數據配置和代碼放到一塊兒,這樣更加直觀, 也更便於修改,至於某些人說 xml配置 能夠省卻編譯打包, 其實在筆者經歷的項目中,不管是改代碼仍是改配置都是須要從新走發佈流程, 嚴禁直接修改配置重啓程序(除極特殊狀況)。
註解和註解解釋器是密不可分的,定義註解以後,首先就應該想到如何定義解釋器,讀取註解上的元數據配置,使用該元數據配置作什麼。
最多見的是使用方式是使用註解註冊某些組件,開啓某項功能,例如 spring 中使用 Component註冊 bean,使用 RequestMapping 註冊 web url 映射, junit 使用 Test 註冊測試 Case, Spring boot 中使用 EnableXXX 開啓某些擴展功能等等,註解解釋器首先須要獲取到 Class 對象使用反射獲取到註解中的元數據配置,而後實現"註冊", "開關"邏輯。 以上在咱們實現的解釋器基類中,咱們也實現了相似的功能,咱們把裝飾器的參數存放到具體的函數對象中, 實際等同於註解的元數據配置, 讀者也能夠擴展, 添加一個標記, 標記該函數對象確實被某裝飾器裝飾過。 這樣便能像 java 同樣輕鬆的實現某些註冊或者開關功能。
除此以外,註解做爲元數據配置,能夠做爲 aop 的切面,這也是註解被普遍使用的緣由, 註解能夠配置在類,屬性,方法之上, "註冊" 功能通常是配置在類上, 若是使用註解切面,須要將註解配置在方法之上。如下列出使用註解 aop 能夠實現的功能
1. 異常攔截 在使用該註解的函數切面上,將異常攔截住,能夠作一些通用的功能,例如異常上報,異常兜底,異常忽略等
2. 權限控制, 日誌記錄。 能夠控制註解方法的切面的用戶訪問權限,也能夠記錄用戶操做
3. 自動重試, 異步處理。若是咱們但願異步調用某方法,或者某些須要異常重試的方法,可使用註解定義切面, 添加異步或重試處理
複製代碼
註解,提供了很是靈活的切面定義方式,以上三種只是常見的使用方式,當註解定義了切面, aop 會替換被代理的類, 添加某些代理邏輯, 拋開底層實現原理, 實際上aop這種機制和 python 的裝飾器區別並非很大, 設計模式中裝飾器和代理模式自己就很是類似, 以上註解能夠實現的功能, python 的裝飾器都是能夠一一實現的。在函數被定義的時刻裝飾器就已經生效了, 而 aop也是經過編譯期或者運行期在實際調用以前代理。 python 的裝飾器自己也是一個函數,它經過語法糖的方式,幫咱們實現了裝飾,而 靜態類型的java 選擇了動態修改字節碼,編譯器織入等更加複雜的技術實現了相似的功能。 不一樣的底層實現, 並不能影響在使用方式及場景上互相借鑑。 因此筆者仍是認爲 裝飾器更像 java 註解+ aop 的組合。 這樣對比 對於java 程序可能更容易理解,更好的使用裝飾器。
更多Java面試資料(操做系統,網絡,zk,mq,redis,java等):github.com/yuhaqiang12…