您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關如何在Python中使用裝飾器,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
Python是一種跨平臺的、具有解釋性、編譯性、互動性和面向對象的腳本語言,其最初的設計是用于編寫自動化腳本,隨著版本的不斷更新和新功能的添加,常用于用于開發獨立的項目和大型項目。
寫裝飾器
裝飾器只不過是一種函數,接收被裝飾的可調用對象作為它的唯一參數,然后返回一個可調用對象(就像前面的簡單例子)
注意重要的一點,當裝飾器被應用到被裝飾函數上時,裝飾器代碼本身就會運行,而不是當被裝飾函數被調用時.理解這個很關鍵,接下來的幾個例子的講解過程也會變得很清楚
第一個例子: 函數注冊
看下面簡單的函數注冊:
registry = [] def register(decorated): registry.append(decorated) return decorated
注冊器方法是一個簡單的裝飾器。它追加位置參數,也就是被裝飾函數到registry變量中,然后不做改變地返回被裝飾方法。任何接受register裝飾器的方法會把它自己追加到registry變量上。
@register def foo(): return 3 @register def bar(): return 5
如果你訪問了registry,可以很容易地在上面迭代并執行里面的函數。
answers = [] for func in registry: answers.append(func())
answers 列表現在回包含 [3, 5]. 這是因為函數已按次序執行,并且它們的返回值被追加到 answers中.
對于現有的函數注冊,有幾類簡單的應用,例如添加“鉤子(hooks)”到代碼中,這樣的話自定義的功能在條件事件之前或之后運行。 下面的Registry類能夠處理這種情況:
class Registry(object): def __init__(self): self._functions = [] def register(self, decorated): self._functions.append(decorated) return decorated def run_all(self, *args, **kwargs): return_values = [] for func in self._functions: return_values.append(func(*args, **kwargs)) return return_values
這個類里的register方法讓然像之前一樣按同樣方法工作。用一個綁定(bound)的方法作為裝飾器完全沒問題。它接收self作為第一參數(像任何綁定方法一樣),并且需要一個額外的位置參數,那就是被裝飾函數,通過創建幾個不同的 registry實例,你可以擁有一些完全分開的注冊器。使用相同函數并且,用超過一個注冊器注冊它也是可行的,像下面展示的一樣 :
a = Registry() b = Registry() @a.register def foo(x=3): return x @b.register def bar(x=5): return x @a.register @b.register def baz(x=7): return x
運行兩個注冊器的run_alll方法,得到如下結果:
a.run_all() # [3, 7] b.run_all() # [5, 7]
注意,run_all 方法能夠使用參數,當它們運行時會把參數傳給內部函數
a.run_all(x=4) # [4, 4]
運行時包裝代碼
以上這些裝飾器都很簡單,因為被裝飾方法被傳遞后未經更改。然而,有些時候當被裝飾方法執行時,你想要運行額外的功能。你通過返回一個添加了相關功能并且在它執行過程中調用被裝飾方法的不同的可調用對象來實現。
簡單的類型檢查
這有一個簡單的裝飾器,確保函數接收到的每一個參數都是整數,否則進行報告:
def requires_ints(decorated): def inner(*args, **kwargs): #獲取任何可能被發送的關鍵值參數 kwarg_values = [i for i in kwargs.values()] #在發送給被裝飾方法的每個值上面進行迭代,確保每一個都是整數; #如果不是拋 TypeError for arg in list(args) + kwarg_values: if not isinstance(arg, int): raise TypeError('%s only accepts integers as arguments.' % decorated.__name__) #運行被裝飾方法,返回結果 return decorated(*args, **kwargs) return inner
發生了什么?
裝飾器是 requires_ints. 它接受一個參數,即被裝飾的可調用對象。這個裝飾器做的唯一事情是返回一個新的可調用對象,一個內部的本地函數。這個函數替代了被裝飾的可調用對象。你可以看到它如何發揮作用,聲明一個函數并且用requires_ints來裝飾
@requires_ints def foo(x, y): """Return the sum of x and y.""" return x + y
注意如果你運行 help(foo)獲取的:
Help on function inner in module __main__: inner(*args, **kwargs) (END)
inner 函數已被指派了名字foo,而不是初始的,已定義了的函數。如果你運行 foo(3, 5), inner 函數會用這些參數來運行,inner函數進行類型檢查,然后運行被裝飾函數,因為inner函數調用它,使用decorated(*args, **kwargs),返回8.沒有這個調用,被裝飾方法會被忽略。
保留helpPreserving the help
一般不想讓裝飾器破壞你的函數的docstring或者操縱help輸出。
因為裝飾器是用來添加通用的和可重用功能的工具,他們有必要更泛化些。
并且,通常來說如果有人使用一個函數試圖在上面運行help,他想要的是關于函數內臟(guts)的信息,而不是外殼(shell)的信息。解決這個問題的方法實際上應用到了 … 仍然是裝飾器. Python 實現了一個叫做 @functools.wraps 的裝飾器,它復制一個函數的內部元素到另一個函數。它把一個函數的重要的內省元素(introspection elements)復制給另一個函數。
這是同一個@requires_ints 裝飾器, 但添加了@functools.wraps的使用:
import functools def requires_ints(decorated): @functools.wraps(decorated) def inner(*args, **kwargs): #獲取可能已作為鍵值參數發送的任何值 kwarg_values = [i for i in kwargs.values()] #迭代發送給被裝飾函數的每個值, 并 #確保每個參數都是整數,否則拋TypeError for arg in args + kwarg_values: if not isinstance(i, int): raise TypeError('%s only accepts integers as arguments.' %decorated.__name__) #運行被裝飾函數然后返回結果 return decorated(*args, **kwargs) return inner
裝飾器本身幾乎沒有改變,除了第二行給inner函數使用了@functools.wraps裝飾器。你現在必須導入functools(在標準庫中)。你也會注意到些額外語法。這個裝飾器實際上使用了一個參數(稍后會有更多)。
現在你可以應用這個裝飾器給相同的函數,像下面這樣:
@requires_ints def foo(x, y): """Return the sum of x and y.""" return x + y
現在當你運行help(foo)的結果:
Help on function foo in module __main__: foo(x, y) Return the sum of x and y. (END)
你看到了 foo的docstring ,同時還有它的方法簽名,然而在蓋頭(hood)下面,@requires_ints裝飾器仍然被應用,并且 inner函數仍然正常運行 。取決于你使用的python版本,運行結果可能稍有不同,尤其當忽略函數簽名時。前面的輸出源自Python 3.4。然而在python 2,提供的函數簽名仍然有點隱秘(因此,是*args和**kwargs而不是x和y)
用戶認證
這個模式(即在運行被裝飾方法前進行過濾驗證)的通常使用場景是用戶認證。考慮一個需要user作為它的第一個參數的方法,user應該是User和AnonymousUser類的實例:
class User(object): """A representation of a user in our application.""" def __init__(self, username, email): self.username = username self.email = email class AnonymousUser(User): """An anonymous user; a stand-in for an actual user that nonetheless is not an actual user. """ def __init__(self): self.username = None self.email = None def __nonzero__(self): return False
裝飾器在此成為隔離用戶驗證的樣板代碼的有力工具。@requires_user裝飾器可以很輕松地認證你獲得了一個User對象并且不是匿名user
import functools def requires_user(func): @functools.wraps(func) def inner(user, *args, **kwargs): """Verify that the user is truthy; if so, run the decorated method, and if not, raise ValueError. """ # Ensure that user is truthy, and of the correct type. # The "truthy"check will fail on anonymous users, since the # AnonymousUser subclass has a ‘__nonzero__‘ method that # returns False. if user and isinstance(user, User): return func(user, *args, **kwargs) else: raise ValueError('A valid user is required to run this.') return inner
這個裝飾器應用了一個通用的,需要樣板化的驗證—-用戶是否登錄進系統的驗證。當你把它作為裝飾器導入,它可重用且易于管理,它應用至函數上也清晰明了。注意這個裝飾器只會正確地包裝一個函數或者靜態方法,如果包裝一個類的綁定方法就會失敗,這是因為裝飾器忽視了發送self作為第一個參數到綁定方法的需要。
格式化輸出
除了過濾一個函數的輸入,裝飾器的另一個用處是過濾一個函數的輸出。當你用Python工作時,只要可能就希望使用Python本地對象。然而通常想要一個序列化的輸出格式(例如,JSON)
在每個相關函數的結尾手動轉換成JSON會顯得很笨(也不是個好主意)。
理想的你應該使用Python數據結構直到需要序列化,但在序列化前仍然可能有其他重復代碼。
裝飾器為這個問題提供了一個出色的,輕便的解決方案。考慮下面的裝飾器,它采用python輸出,并序列化結果為JSON
import functools import json def json_output(decorated): """Run the decorated function, serialize the result of that function to JSON, and return the JSON string. """ @functools.wraps(decorated) def inner(*args, **kwargs): result = decorated(*args, **kwargs) return json.dumps(result) return inner
給一個 簡單函數應用@json_output
裝飾器 :
@json_output def do_nothing(): return {'status': 'done'}
在Python shell中運行這個函數:
>>> do_nothing() '{"status": "done"}'
結果是一個包含JSON的字符串,而不是一個字典。
這個裝飾器的優美在于它的簡潔。把這個裝飾器應用到一個函數,本來返回python字典,列表或者其它對象的函數現在會返回它的JSON序列化的版本。你可能會問這有什么價值?畢竟你加了一行裝飾器,實質上只移除了一行調用json.dumps的代碼。
然而,由于應用的需求會擴展,還是考慮一下擁有此裝飾器的價值。
例如,某種異常需要被捕獲,并以特定的格式化的json輸出,而不是讓異常上浮產生堆棧跟蹤,該怎么做?因為有裝飾器,這個功能很容易添加。
import functools import json class JSONOutputError(Exception): def __init__(self, message): self._message = message def __str__(self): return self._message def json_output(decorated): """Run the decorated function, serialize the result of that function to JSON, and return the JSON string. """ @functools.wraps(decorated) def inner(*args, **kwargs): try: result = decorated(*args, **kwargs) except JSONOutputError as ex: result = { 'status': 'error', 'message': str(ex), } return json.dumps(result) return inner
通過使用錯誤處理增強@json_output裝飾器,你已經把該功能添加給了應用了這個裝飾器的任何函數。
這是讓裝飾器如此有價值的部分原因。對于代碼輕便化,可重用化而言,它們是非常有用的工具。
現在,如果一個用@json_output裝飾的函數拋出了JSONOutputError異常,就會有特別的錯誤處理:
@json_output def error(): raise JSONOutputError('This function is erratic.')
運行error 函數:
>>> error() '{"status": "error", "message": "This function is erratic."}'
注意,只有JSONOutputError異常類(或它的子類)會獲得這種特別的錯誤處理。任何其它異常會正常通過,并產生堆棧跟蹤。
實質上,裝飾器是避免重復你自己的工具,并且它們的部分價值在于給未來的維護提供鉤子(hooks)。這些不用裝飾器也可以實現,考慮要求用戶登錄進系統的例子,寫一個函數并把它放在需要這項功能的函數的入口處就行了。裝飾器首先是一種語法糖(syntactic sugar)。然而是一種很有價值的語法糖。畢竟,相較于寫,代碼更多時候用來讀,而且你可以一眼定位到裝飾器的位置。
日志記錄Logging
執行時包裝代碼的最后一個例子是一個通用的日志記錄函數。 考慮下面引起函數調用的裝飾器, 運行時間,
結果會被記錄:
import functools import logging import time def logged(method): """Cause the decorated method to be run and its results logged, along with some other diagnostic information. """ @functools.wraps(method) def inner(*args, **kwargs): #Record our start time. start = time.time() #Run the decorated method. return_value = method(*args, **kwargs) #Record our completion time, and calculate the delta. end = time.time() delta = end - start #Log the method call and the result. logger = logging.getLogger('decorator.logged') logger.warn('Called method %s at %.02f; execution time %.02f seconds; result %r.' % (method.__name__, start, delta, return_value)) #Return the methods original return value. return return_value return inner
當應用到一個函數上后,這個裝飾器正常地運行那個函數,但函數調用結束后會使用Python logging模塊記錄信息。
>>> import time >>> @logged… def sleep_and_return(return_value): ... time.sleep(2) ... return return_value… >>> >>> sleep_and_return(42) Called method sleep_and_return at 1424462194.70; execution time 2.00 seconds; result 42. 42
不像先前的例子,這個裝飾器不顯式地更改函數調用. 不存在你應用這個裝飾器后獲得的結果與沒有被裝飾的函數的結果不一樣的情況。這個裝飾器做了些幕后工作,但并不改變實際結果。
值得注意的是, @json_output 和 @logged 裝飾器都提供 inner 函數 ,這個函數簡單地以最小的偵測采用和傳遞可變參數和關鍵字參數。
這是一種重要的模式。一種它尤其重要的方式是,許多裝飾器可能被用來裝飾純粹的函數和類的方法。記住,在Python中,類中聲明的方法會獲得一個額外位置參數,即廣為人知的self。當裝飾器在使用時,它不會改變 (這就是為什么先前的requires_user裝飾器在類的綁定方法上不起作用)
例如@json_result被用來裝飾一個類的方法,inner函數被調用,它接收一個類的實例作為第一個參數。實際上這沒有問題。在這種情況下,這個參數就是args[0],它被傳送給被裝飾方法。
裝飾器參數
到目前為止列出的所有裝飾器都沒有任何參數。作為討論過的內容,有一個暗含的參數–被裝飾的方法。然而,有時讓裝飾器自身使用一些它需要的信息去裝飾相關方法會有用處。
一個參數傳給一個裝飾器和一個參數傳給一個正在調用的方法之間的不同是,當一個函數被聲明并被裝飾,傳給裝飾器的參數會被立刻處理。相反,傳給函數的參數在函數調用時被處理。通過@functools.wraps
的多次使用,你已經看到了一個參數傳給裝飾器的例子。它使用一個參數—-被包裝的方法,方法的help和docstring等類似的東西應該被保留。然而,裝飾器有內含的調用簽名。他們使用一個位置參數–被裝飾的方法。所以,這是怎么工作的?答案說來就復雜了。
回想運行時包裝代碼的基本裝飾器 ,他們在局部范圍聲明了一個 inner 方法 然后返回它. 這就是由裝飾器返回的可調用對象. 它被指派了被調用函數的名字. 使用參數的裝飾器多添加一個包裝層,這是因為,使用參數的裝飾器不再是一個實際的裝飾器。它是一個返回裝飾器的函數,是一個使用一個參數(被裝飾的方法)的函數。然后裝飾函數并返回一個可調用對象。聽起來混亂,考慮下面的例子,在這里,裝飾器@json_output
的功能被增強了,要求縮進和排序:
import json class JSONOutputError(Exception): def __init__(self, message): self._message = message def __str__(self): return self._message def json_output(indent=None, sort_keys=False): """Run the decorated function, serialize the result of that function to JSON, and return the JSON string. """ def actual_decorator(decorated): @functools.wraps(decorated) def inner(*args, **kwargs): try: result = decorated(*args, **kwargs) except JSONOutputError as ex: result = { 'status': 'error', 'message': str(ex), } return json.dumps(result, indent=indent, sort_keys=sort_keys) return inner return actual_decorator
那么,發生了什么,為什么這回起作用?這是一個函數,json_output,接收兩個參數(indent 和 sort_keys). 它返回另一個函數, 叫 actual_decorator, 這是 (如同名字表名的) 要作為裝飾器使用的. 這是一個典型的裝飾器—一個接收可調用對象(被裝飾的)做參數的可調用對象,并且返回一個可調用對象(inner).
注意函數已經有所改變來容納indent和sort_keys參數。
inner 函數是最終使用 indent 和 sort_keys 參數的. 這沒有問題,因為Python的塊作用域規則允許這樣。使用不同的indent和sort_keys來調用也不成問題,因為inner是本地函數(每次裝飾器被使用都會返回一個不同的副本)應用 json_output 函數:
@json_output(indent=4) def do_nothing(): return {'status': 'done'}
現在運行do_nothing , 會產生一個帶縮進的JSON:
>>> do_nothing() '{\n "status": "done"\n}'
這是怎么起作用的?
但是,等一等. 如果json_output 不是一個裝飾器, 而是一個返回裝飾器的函數,為什么它使用起來看著像是一個裝飾器?在這里,Python解釋器做了什么來讓它工作的?更多的解釋已經就緒。在這的關鍵是操作順序。
特別地,函數調用(json_output(indent=4)
) 先于裝飾器應用語法(@
)被處理 。因此,函數調用的結果會應用給裝飾器。
發生的第一件事情是解釋器尋找 json_output 函數調用,然后解析這個調用:
@json_output(indent=4) def do_nothing(): return {'status': 'done'}
json_output 函數所要做的一切就是定義另一個函數, actual_decorator, 然后返回它. 這個函數的結果會提供給@,像下面這樣:
@actual_decorator def do_nothing(): return {'status': 'done'}
現在, actual_decorator 在運行. 它聲明另一個本地函數, inner, 并返回它. 像先前討論過的,這個函數會被指派名字 do_nothing, 被裝飾方法的名字. 當do_nothing被調用, inner 函數就會被調用, 運行被裝飾方法, JSON使用合適縮進 調用dumps 處理結果
調用簽名很重要
當你引進了你的新的,更改過后的json_output函數,你實際引進了一個反向不兼容(backward-incompatible )的改變,意識到這點很重要。
為什么?因為現在期待一個額外的函數調用。如果你想要舊的json_output的行為,不需要任何可用的參數的值,你仍然必須調用這個方法
換句話說,你必須像下面這樣做:
@json_output() def do_nothing(): return {'status': 'done'}
注意圓括號. 它們有影響,因為它們指出了函數正在被調用(即便沒有參數),然后結果應用給@.
前面的代碼不等價于下面:
@json_output def do_nothing(): return {'status': 'done'}
這呈現出兩個問題。有點讓人迷惑,如果你習慣于看到不帶簽名的裝飾器的應用,提供一個空簽名的需要就違背直覺。
第二,如果舊的裝飾器在你的應用中已經存在,你必須返回并編輯所有它們的現有的調用。如果可能的話,你應該避免反向不減容(backward-incompatible)改變。
完美的情況下,下面三種不同的使用方式,裝飾器都會工作
@json_output @json_output() @json_output(indent=4)
讓裝飾器基于接收到的參數來改變它的行為是可能的。記住,裝飾器只是一個函數,擁有任何其它函數所擁有的所有靈活性,包括對它獲取到的輸入做出需要做出的響應。
考慮這個對 json_output的更加靈活的迭代:
import functools import json class JSONOutputError(Exception): def __init__(self, message): self._message = message def __str__(self): return self._message def json_output(decorated_=None, indent=None, sort_keys=False): """Run the decorated function, serialize the result of that function to JSON, and return the JSON string. """ # Did we get both a decorated method and keyword arguments? # That should not happen. if decorated_ and (indent or sort_keys): raise RuntimeError('Unexpected arguments.') # Define the actual decorator function. def actual_decorator(func): @functools.wraps(func) def inner(*args, **kwargs): try: result = func(*args, **kwargs) except JSONOutputError as ex: result = { 'status': 'error', 'message': str(ex), } return json.dumps(result, indent=indent, sort_keys=sort_keys) return inner #Return either the actual decorator, or the result of applying #the actual decorator, depending on what arguments we got. if decorated_: return actual_decorator(decorated_) else: return actual_decorator
在目前是不是正作為裝飾器使用這一方面,這個函數正努力變得智能。
首先,它確保它不會以出乎意料的方式被調用
你永遠不要期待接收被裝飾方法同時關鍵值參數,因為裝飾器被調用時總是以被裝飾方法作為唯一參數。
第二,它定義了actual_decorator函數,這是要被返回和應用的實際裝飾器。它定義了inner 函數,它時從裝飾器中返回的最終函數。
最終, 它返回合適結果,這基于它被如何調用:
如果 設置了decorated_ , 它會被作為純粹的裝飾器調用, 沒有方法簽名,然后它的響應應用給最終裝飾器并返回inner函數 . 再次注意使用參數的裝飾器如何實際地運作。首先, actual_decorator(decorated_)
被調用,解析。然后它的結果(必須是一個可調用對象,因為這是一個裝飾器)被調用,inner被提供作為唯一的參數。
如果decorated_沒被設置,就會使用關鍵字參數調用,這個函數必須返回一個實際的裝飾器,它接收被裝飾方法,并返回inner。因此,這個函數返回actual_decorator
然后這會被python解釋器作為實際裝飾器(最終返回inner)
為何這個技術有價值?它讓你能夠先先前使用過的一樣管理你的裝飾器的功能。意味著你不用去更新已經應用了裝飾器的每個地方,但仍然獲得了在你需要時添加參數的靈活性。
關于如何在Python中使用裝飾器就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。