深入理解Python中裝飾器的用法,深入理解python
因為函數或類都是對象,它們也能被四處傳遞。它們又是可變對象,可以被更改。在函數或類對象建立後但綁定到名字前更改之的行為為裝飾(decorator)。
“裝飾器”後隱藏了兩種意思——一是函數起了裝飾作用,例如,執行真正的工作,另一個是依附於裝飾器文法的運算式,例如,at符號和裝飾函數的名稱。
函數可以通過函數裝飾器文法裝飾:
@decorator # ②def function(): # ① pass
函數以標準方式定義。①
以@做為定義為裝飾器函數首碼的運算式②。在 @ 後的部分必須是簡單的運算式,通常只是函數或類的名字。這一部分先求值,在下面的定義的函數準備好後,裝飾器被新定義的函數對象作為單個參數調用。裝飾器返回的值附著到被裝飾的函數名。
裝飾器可以應用到函數和類上。對類語義很明晰——類定義被當作參數來調用裝飾器,無論返回什麼都賦給被裝飾的名字。在裝飾器文法實現前(PEP 318),通過將函數和類對象賦給臨時變數然後顯式調用裝飾器然後將傳回值賦給函數名,可以完成同樣的事。這似乎要打更多的字,也確實裝飾器函數名用了兩次同時臨時變數要用至少三次,很容易出錯。以上執行個體相當於:
def function(): # ① passfunction = decorator(function) # ②
裝飾器可以堆棧(stacked)——應用的順序是從底到上或從裡到外。就是說最初的函數被當作第一次參數器的參數,無論返回什麼都被作為第二個裝飾器的參數……無論最後一個裝飾器返回什麼都被依附到最初函數的名下。
裝飾器文法因其可讀性被選擇。因為裝飾器在函數頭部前被指定,顯然不是函數體的一部分,它只能對整個函數起作用。以@為首碼的運算式又讓它明顯到不容忽視(根據PEP叫在您臉上……:))。當多個裝飾器被應用時,每個放在不同的行非常易於閱讀。
代替和調整原始對象
裝飾器可以或者返回相同的函數或類對象或者返回完全不同的對象。第一種情況中,裝飾器利用函數或類對象是可變的添加屬性,例如向類添加文檔字串(docstring).裝飾器甚至可以在不改變對象的情況下做有用的事,例如在全域註冊表中註冊裝飾的類。在第二種情況中,簡直無所不能:當什麼不同的東西取代了被裝飾的類或函數,新對象可以完全不同。然而這不是裝飾器的目的:它們意在改變裝飾對象而非做不可預料的事。因此當一個函數在裝飾時被完全替代成不同的函數時,新函數通常在一些準備工作後調用原始函數。同樣,當一個類被裝飾成一個新類時,新類通常源於被裝飾類。當裝飾器的目的是“每次都”做什麼,像記錄每次對被裝飾函數的調用,只有第二類裝飾器可用。另一方面,如果第一類足夠了,最好使用它因為更簡單。
實作類別和函數裝飾器
對裝飾器惟一的要求是它能夠單參數調用。這意味著裝飾器可以作為常規函數或帶有__call__方法的類的實現,理論上,甚至lambda函數也行。
讓我們比較函數和類方法。裝飾器運算式(@後部分)可以只是名字。只有名字的方法很好(打字少,看起來整潔等),但是只有當無需用參數定製裝飾器時才可能。被寫作函數的裝飾器可以用以下兩種方式:
>>> def simple_decorator(function):... print "doing decoration"... return function>>> @simple_decorator... def function():... print "inside function"doing decoration>>> function()inside function>>> def decorator_with_arguments(arg):... print "defining the decorator"... def _decorator(function):... # in this inner function, arg is available too... print "doing decoration,", arg... return function... return _decorator>>> @decorator_with_arguments("abc")... def function():... print "inside function"defining the decoratordoing decoration, abc>>> function()inside function
這兩個裝飾器屬於返回被裝飾函數的類別。如果它們想返回新的函數,需要額外的嵌套,最糟的情況下,需要三層嵌套。
>>> def replacing_decorator_with_args(arg):... print "defining the decorator"... def _decorator(function):... # in this inner function, arg is available too... print "doing decoration,", arg... def _wrapper(*args, **kwargs):... print "inside wrapper,", args, kwargs... return function(*args, **kwargs)... return _wrapper... return _decorator>>> @replacing_decorator_with_args("abc")... def function(*args, **kwargs):... print "inside function,", args, kwargs... return 14defining the decoratordoing decoration, abc>>> function(11, 12)inside wrapper, (11, 12) {}inside function, (11, 12) {}14
_wrapper函數被定義為接受所有位置和關鍵字參數。通常我們不知道哪些參數被裝飾函數會接受,所以wrapper將所有東西都創遞給被裝飾函數。一個不幸的結果就是顯式參數很迷惑人。
相比定義為函數的裝飾器,定義為類的複雜裝飾器更簡單。當對象被建立,__init__方法僅僅允許返回None,建立的物件類型不能更改。這意味著當裝飾器被定義為類時,使用無參數的形式沒什麼意義:最終被裝飾的對象只是裝飾類的一個執行個體而已,被構建器(constructor)調用返回,並不非常有用。討論在裝飾運算式中給出參數的基於類的裝飾器,__init__方法被用來構建裝飾器。
>>> class decorator_class(object):... def __init__(self, arg):... # this method is called in the decorator expression... print "in decorator init,", arg... self.arg = arg... def __call__(self, function):... # this method is called to do the job... print "in decorator call,", self.arg... return function>>> deco_instance = decorator_class('foo')in decorator init, foo>>> @deco_instance... def function(*args, **kwargs):... print "in function,", args, kwargsin decorator call, foo>>> function()in function, () {}
相對於正常規則(PEP 8)由類寫成的裝飾器表現得更像函數,因此它們的名字以小寫字母開始。
事實上,建立一個僅返回被裝飾函數的新類沒什麼意義。對象應該有狀態,這種裝飾器在裝飾器返回新對象時更有用。
>>> class replacing_decorator_class(object):... def __init__(self, arg):... # this method is called in the decorator expression... print "in decorator init,", arg... self.arg = arg... def __call__(self, function):... # this method is called to do the job... print "in decorator call,", self.arg... self.function = function... return self._wrapper... def _wrapper(self, *args, **kwargs):... print "in the wrapper,", args, kwargs... return self.function(*args, **kwargs)>>> deco_instance = replacing_decorator_class('foo')in decorator init, foo>>> @deco_instance... def function(*args, **kwargs):... print "in function,", args, kwargsin decorator call, foo>>> function(11, 12)in the wrapper, (11, 12) {}in function, (11, 12) {}
像這樣的裝飾器可以做任何事,因為它能改變被裝飾函數對象和參數,調用被裝飾函數或不調用,最後改變傳回值。
複製原始函數的文檔字串和其它屬性
當新函數被返回代替裝飾前的函數時,不幸的是原函數的函數名,文檔字串和參數列表都丟失了。這些屬性可以部分通過設定__doc__(文檔字串),__module__和__name__(函數的全稱)、__annotations__(Python 3中關於參數和傳回值的額外資訊)移植到新函數上,這些工作可通過functools.update_wrapper自動完成。
>>> import functools>>> def better_replacing_decorator_with_args(arg):... print "defining the decorator"... def _decorator(function):... print "doing decoration,", arg... def _wrapper(*args, **kwargs):... print "inside wrapper,", args, kwargs... return function(*args, **kwargs)... return functools.update_wrapper(_wrapper, function)... return _decorator>>> @better_replacing_decorator_with_args("abc")... def function():... "extensive documentation"... print "inside function"... return 14defining the decoratordoing decoration, abc>>> function <function function at 0x...>>>> print function.__doc__extensive documentation
一件重要的東西是從可遷移屬性列表中所缺少的:參數列表。參數的預設值可以通過__defaults__、__kwdefaults__屬性更改,但是不幸的是參數列表本身不能被設定為屬性。這意味著help(function)將顯式無用的參數列表,使使用者迷惑不已。一個解決此問題有效但是醜陋的方式是使用eval動態建立wrapper。可以使用外部external模組自動實現。它提供了對decorator裝飾器的支援,該裝飾器接受wrapper並將之轉換成保留函數簽名的裝飾器。
綜上,裝飾器應該總是使用functools.update_wrapper或者其它方式賦值函數屬性。
標準庫中的樣本
首先要提及的是標準庫中有一些實用的裝飾器,有三種裝飾器:
classmethod讓一個方法變成“類方法”,即它能夠無需建立執行個體調用。當一個常規方法被調用時,解譯器插入執行個體對象作為第一個參數self。當類方法被調用時,類本身被給做第一個參數,一般叫cls。
類方法也能通過類命名空間讀取,所以它們不必汙染模組命名空間。類方法可用來提供替代的構建器(constructor):
class Array(object): def __init__(self, data): self.data = data @classmethod def fromfile(cls, file): data = numpy.load(file) return cls(data)
這比用一大堆標記的__init__簡單多了。
staticmethod應用到方法上讓它們“靜態”,例如,本來一個常規函數,但通過類命名空間存取。這在函數僅在類中需要時有用(它的名字應該以_為首碼),或者當我們想要使用者以為方法串連到類時也有用——雖然對實現本身不必要。
property是對getter和setter問題Python風格的答案。通過property裝飾的方法變成在屬性存取時自動調用的getter。
>>> class A(object):... @property... def a(self):... "an important attribute"... return "a value">>> A.a <property object at 0x...>>>> A().a'a value'
例如A.a是唯讀屬性,它已經有文檔了:help(A)包含從getter方法擷取的屬性a的文檔字串。將a定義為property使它能夠直接被計算,並且產生唯讀副作用,因為沒有定義任何setter。
為了得到setter和getter,顯然需要兩個方法。從Python 2.6開始首選以下文法:
class Rectangle(object): def __init__(self, edge): self.edge = edge @property def area(self): """Computed area. Setting this updates the edge length to the proper value. """ return self.edge**2 @area.setter def area(self, area): self.edge = area ** 0.5
通過property裝飾器取代帶一個屬性(property)對象的getter方法,以上代碼起作用。這個對象反過來有三個可用於裝飾器的方法getter、setter和deleter。它們的作用就是設定屬性對象的getter、setter和deleter(被儲存為fget、fset和fdel屬性(attributes))。當建立對象時,getter可以像上例一樣設定。當定義setter時,我們已經在area中有property對象,可以通過setter方法向它添加setter,一切都在建立類時完成。
之後,當類執行個體建立後,property對象和特殊。當解譯器執行屬性存取、賦值或刪除時,其執行被下放給property對象的方法。
為了讓一切一清二楚[^5],讓我們定義一個“調試”例子:
>>> class D(object):... @property... def a(self):... print "getting", 1... return 1... @a.setter... def a(self, value):... print "setting", value... @a.deleter... def a(self):... print "deleting">>> D.a <property object at 0x...>>>> D.a.fget <function a at 0x...>>>> D.a.fset <function a at 0x...>>>> D.a.fdel <function a at 0x...>>>> d = D() # ... varies, this is not the same `a` function>>> d.agetting 11>>> d.a = 2setting 2>>> del d.adeleting>>> d.agetting 11
屬性(property)是對裝飾器文法的一點擴充。使用裝飾器的一大前提——命名不重複——被違反了,但是目前沒什麼更好的發明。為getter,setter和deleter方法使用相同的名字還是個好的風格。
一些其它更新的例子包括:
functools.lru_cache記憶任意維持有限 參數:結果 對的緩衝函數(Python
3.2)
functools.total_ordering是一個基於單個比較方法而填充丟失的比較(ordering)方法(__lt__,__gt__,__le__等等)的類裝飾器。
函數的廢棄
比如說我們想在第一次調用我們不希望被調用的函數時在標準錯誤列印一個廢棄函數警告。如果我們不想更改函數,我們可用裝飾器
class deprecated(object): """Print a deprecation warning once on first use of the function. >>> @deprecated() # doctest: +SKIP ... def f(): ... pass >>> f() # doctest: +SKIP f is deprecated """ def __call__(self, func): self.func = func self.count = 0 return self._wrapper def _wrapper(self, *args, **kwargs): self.count += 1 if self.count == 1: print self.func.__name__, 'is deprecated' return self.func(*args, **kwargs)
也可以實現成函數:
def deprecated(func): """Print a deprecation warning once on first use of the function. >>> @deprecated # doctest: +SKIP ... def f(): ... pass >>> f() # doctest: +SKIP f is deprecated """ count = [0] def wrapper(*args, **kwargs): count[0] += 1 if count[0] == 1: print func.__name__, 'is deprecated' return func(*args, **kwargs) return wrapper
while-loop移除裝飾器
例如我們有個返回列表的函數,這個列表由迴圈建立。如果我們不知道需要多少對象,實現這個的標準方法如下:
def find_answers(): answers = [] while True: ans = look_for_next_answer() if ans is None: break answers.append(ans) return answers
只要迴圈體很緊湊,這很好。一旦事情變得更複雜,正如真實的代碼中發生的那樣,這就很難讀懂了。我們可以通過yield語句簡化它,但之後使用者不得不顯式調用嗯list(find_answers())。
我們可以建立一個為我們構建列表的裝飾器:
def vectorized(generator_func): def wrapper(*args, **kwargs): return list(generator_func(*args, **kwargs)) return functools.update_wrapper(wrapper, generator_func)
然後函數變成這樣:
@vectorizeddef find_answers(): while True: ans = look_for_next_answer() if ans is None: break yield ans
外掛程式註冊系統
這是一個僅僅把它放進全域註冊表中而不更改類的類裝飾器,它屬於返回被裝飾對象的裝飾器。
class WordProcessor(object): PLUGINS = [] def process(self, text): for plugin in self.PLUGINS: text = plugin().cleanup(text) return text @classmethod def plugin(cls, plugin): cls.PLUGINS.append(plugin)@WordProcessor.pluginclass CleanMdashesExtension(object): def cleanup(self, text): return text.replace('—', u'\N{em dash}')
這裡我們使用裝飾器完成外掛程式註冊。我們通過一個名詞調用裝飾器而不是一個動詞,因為我們用它來聲明我們的類是WordProcessor的一個外掛程式。plugin方法僅僅將類添加進外掛程式列表。
關於外掛程式自身說下:它用真正的Unicode中的破折號符號替代HTML中的破折號。它利用unicode literal notation通過它在unicode資料庫中的名稱(“EM DASH”)插入一個符號。如果直接插入Unicode符號,將不可能區分所插入的和來源程式中的破折號。