深入理解Python中裝飾器的用法
因為函數(shù)或類都是對象,它們也能被四處傳遞。它們又是可變對象,可以被更改。在函數(shù)或類對象創(chuàng)建后但綁定到名字前更改之的行為為裝飾(decorator)。
“裝飾器”后隱藏了兩種意思——一是函數(shù)起了裝飾作用,例如,執(zhí)行真正的工作,另一個是依附于裝飾器語法的表達式,例如,at符號和裝飾函數(shù)的名稱。
函數(shù)可以通過函數(shù)裝飾器語法裝飾:
@decorator # ② def function(): # ① pass
以@做為定義為裝飾器函數(shù)前綴的表達式②。在 @ 后的部分必須是簡單的表達式,通常只是函數(shù)或類的名字。這一部分先求值,在下面的定義的函數(shù)準備好后,裝飾器被新定義的函數(shù)對象作為單個參數(shù)調(diào)用。裝飾器返回的值附著到被裝飾的函數(shù)名。
裝飾器可以應用到函數(shù)和類上。對類語義很明晰——類定義被當作參數(shù)來調(diào)用裝飾器,無論返回什么都賦給被裝飾的名字。
def function(): # ① pass function = decorator(function) # ②
裝飾器語法因其可讀性被選擇。因為裝飾器在函數(shù)頭部前被指定,顯然不是函數(shù)體的一部分,它只能對整個函數(shù)起作用。以@為前綴的表達式又讓它明顯到不容忽視(根據(jù)PEP叫在您臉上……:))。當多個裝飾器被應用時,每個放在不同的行非常易于閱讀。
代替和調(diào)整原始對象
裝飾器可以或者返回相同的函數(shù)或類對象或者返回完全不同的對象。第一種情況中,裝飾器利用函數(shù)或類對象是可變的添加屬性,例如向類添加文檔字符串(docstring).裝飾器甚至可以在不改變對象的情況下做有用的事,例如在全局注冊表中注冊裝飾的類。在第二種情況中,簡直無所不能:當什么不同的東西取代了被裝飾的類或函數(shù),新對象可以完全不同。然而這不是裝飾器的目的:它們意在改變裝飾對象而非做不可預料的事。因此當一個函數(shù)在裝飾時被完全替代成不同的函數(shù)時,新函數(shù)通常在一些準備工作后調(diào)用原始函數(shù)。同樣,當一個類被裝飾成一個新類時,新類通常源于被裝飾類。當裝飾器的目的是“每次都”做什么,像記錄每次對被裝飾函數(shù)的調(diào)用,只有第二類裝飾器可用。另一方面,如果第一類足夠了,最好使用它因為更簡單。
實現(xiàn)類和函數(shù)裝飾器
對裝飾器惟一的要求是它能夠單參數(shù)調(diào)用。這意味著裝飾器可以作為常規(guī)函數(shù)或帶有__call__方法的類的實現(xiàn),理論上,甚至lambda函數(shù)也行。
讓我們比較函數(shù)和類方法。裝飾器表達式(@后部分)可以只是名字。只有名字的方法很好(打字少,看起來整潔等),但是只有當無需用參數(shù)定制裝飾器時才可能。被寫作函數(shù)的裝飾器可以用以下兩種方式:
>>> 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 decorator doing decoration, abc >>> function() inside function
這兩個裝飾器屬于返回被裝飾函數(shù)的類別。如果它們想返回新的函數(shù),需要額外的嵌套,最糟的情況下,需要三層嵌套。
>>> 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 14 defining the decorator doing decoration, abc >>> function(11, 12) inside wrapper, (11, 12) {} inside function, (11, 12) {} 14
_wrapper函數(shù)被定義為接受所有位置和關(guān)鍵字參數(shù)。通常我們不知道哪些參數(shù)被裝飾函數(shù)會接受,所以wrapper將所有東西都創(chuàng)遞給被裝飾函數(shù)。一個不幸的結(jié)果就是顯式參數(shù)很迷惑人。
相比定義為函數(shù)的裝飾器,定義為類的復雜裝飾器更簡單。當對象被創(chuàng)建,__init__方法僅僅允許返回None,創(chuàng)建的對象類型不能更改。這意味著當裝飾器被定義為類時,使用無參數(shù)的形式?jīng)]什么意義:最終被裝飾的對象只是裝飾類的一個實例而已,被構(gòu)建器(constructor)調(diào)用返回,并不非常有用。討論在裝飾表達式中給出參數(shù)的基于類的裝飾器,__init__方法被用來構(gòu)建裝飾器。
>>> 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, kwargs in decorator call, foo >>> function() in function, () {}
相對于正常規(guī)則(PEP 8)由類寫成的裝飾器表現(xiàn)得更像函數(shù),因此它們的名字以小寫字母開始。
事實上,創(chuàng)建一個僅返回被裝飾函數(shù)的新類沒什么意義。對象應該有狀態(tài),這種裝飾器在裝飾器返回新對象時更有用。
>>> 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, kwargs in decorator call, foo >>> function(11, 12) in the wrapper, (11, 12) {} in function, (11, 12) {}
像這樣的裝飾器可以做任何事,因為它能改變被裝飾函數(shù)對象和參數(shù),調(diào)用被裝飾函數(shù)或不調(diào)用,最后改變返回值。
復制原始函數(shù)的文檔字符串和其它屬性
當新函數(shù)被返回代替裝飾前的函數(shù)時,不幸的是原函數(shù)的函數(shù)名,文檔字符串和參數(shù)列表都丟失了。這些屬性可以部分通過設置__doc__(文檔字符串),__module__和__name__(函數(shù)的全稱)、__annotations__(Python 3中關(guān)于參數(shù)和返回值的額外信息)移植到新函數(shù)上,這些工作可通過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 14 defining the decorator doing decoration, abc >>> function <function function at 0x...> >>> print function.__doc__ extensive documentation
一件重要的東西是從可遷移屬性列表中所缺少的:參數(shù)列表。參數(shù)的默認值可以通過__defaults__、__kwdefaults__屬性更改,但是不幸的是參數(shù)列表本身不能被設置為屬性。這意味著help(function)將顯式無用的參數(shù)列表,使使用者迷惑不已。一個解決此問題有效但是丑陋的方式是使用eval動態(tài)創(chuàng)建wrapper??梢允褂猛獠縠xternal模塊自動實現(xiàn)。它提供了對decorator裝飾器的支持,該裝飾器接受wrapper并將之轉(zhuǎn)換成保留函數(shù)簽名的裝飾器。
綜上,裝飾器應該總是使用functools.update_wrapper或者其它方式賦值函數(shù)屬性。
標準庫中的示例
首先要提及的是標準庫中有一些實用的裝飾器,有三種裝飾器:
classmethod讓一個方法變成“類方法”,即它能夠無需創(chuàng)建實例調(diào)用。當一個常規(guī)方法被調(diào)用時,解釋器插入實例對象作為第一個參數(shù)self。當類方法被調(diào)用時,類本身被給做第一個參數(shù),一般叫cls。
類方法也能通過類命名空間讀取,所以它們不必污染模塊命名空間。類方法可用來提供替代的構(gòu)建器(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應用到方法上讓它們“靜態(tài)”,例如,本來一個常規(guī)函數(shù),但通過類命名空間存取。這在函數(shù)僅在類中需要時有用(它的名字應該以_為前綴),或者當我們想要用戶以為方法連接到類時也有用——雖然對實現(xiàn)本身不必要。
property是對getter和setter問題Python風格的答案。通過property裝飾的方法變成在屬性存取時自動調(diào)用的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是只讀屬性,它已經(jīng)有文檔了:help(A)包含從getter方法獲取的屬性a的文檔字符串。將a定義為property使它能夠直接被計算,并且產(chǎn)生只讀的副作用,因為沒有定義任何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))。當創(chuàng)建對象時,getter可以像上例一樣設定。當定義setter時,我們已經(jīng)在area中有property對象,可以通過setter方法向它添加setter,一切都在創(chuàng)建類時完成。
之后,當類實例創(chuàng)建后,property對象和特殊。當解釋器執(zhí)行屬性存取、賦值或刪除時,其執(zhí)行被下放給property對象的方法。
為了讓一切一清二楚[^5],讓我們定義一個“調(diào)試”例子:
>>> 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.a getting 1 1 >>> d.a = 2 setting 2 >>> del d.a deleting >>> d.a getting 1 1
屬性(property)是對裝飾器語法的一點擴展。使用裝飾器的一大前提——命名不重復——被違反了,但是目前沒什么更好的發(fā)明。為getter,setter和deleter方法使用相同的名字還是個好的風格。
一些其它更新的例子包括:
functools.lru_cache記憶任意維持有限 參數(shù):結(jié)果 對的緩存函數(shù)(Python
3.2)
functools.total_ordering是一個基于單個比較方法而填充丟失的比較(ordering)方法(__lt__,__gt__,__le__等等)的類裝飾器。
函數(shù)的廢棄
比如說我們想在第一次調(diào)用我們不希望被調(diào)用的函數(shù)時在標準錯誤打印一個廢棄函數(shù)警告。如果我們不想更改函數(shù),我們可用裝飾器
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)
也可以實現(xiàn)成函數(shù):
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移除裝飾器
例如我們有個返回列表的函數(shù),這個列表由循環(huán)創(chuàng)建。如果我們不知道需要多少對象,實現(xiàn)這個的標準方法如下:
def find_answers(): answers = [] while True: ans = look_for_next_answer() if ans is None: break answers.append(ans) return answers
只要循環(huán)體很緊湊,這很好。一旦事情變得更復雜,正如真實的代碼中發(fā)生的那樣,這就很難讀懂了。我們可以通過yield語句簡化它,但之后用戶不得不顯式調(diào)用嗯list(find_answers())。
我們可以創(chuàng)建一個為我們構(gòu)建列表的裝飾器:
def vectorized(generator_func): def wrapper(*args, **kwargs): return list(generator_func(*args, **kwargs)) return functools.update_wrapper(wrapper, generator_func)
然后函數(shù)變成這樣:
@vectorized def find_answers(): while True: ans = look_for_next_answer() if ans is None: break yield ans
插件注冊系統(tǒng)
這是一個僅僅把它放進全局注冊表中而不更改類的類裝飾器,它屬于返回被裝飾對象的裝飾器。
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.plugin class CleanMdashesExtension(object): def cleanup(self, text): return text.replace('—', u'\N{em dash}')
這里我們使用裝飾器完成插件注冊。我們通過一個名詞調(diào)用裝飾器而不是一個動詞,因為我們用它來聲明我們的類是WordProcessor的一個插件。plugin方法僅僅將類添加進插件列表。
關(guān)于插件自身說下:它用真正的Unicode中的破折號符號替代HTML中的破折號。它利用unicode literal notation通過它在unicode數(shù)據(jù)庫中的名稱(“EM DASH”)插入一個符號。如果直接插入Unicode符號,將不可能區(qū)分所插入的和源程序中的破折號。
相關(guān)文章
Python調(diào)試神器之PySnooper的使用教程分享
對于每個程序開發(fā)者來說,調(diào)試幾乎是必備技能。本文小編就來給大家介紹一款非常好用的調(diào)試工具,它能在一些場景下,大幅度提高調(diào)試的效率, 那就是 PySnooper,希望大家喜歡2023-02-02Python自動創(chuàng)建Markdown表格使用實例探究
Markdown表格是文檔中整理和展示數(shù)據(jù)的重要方式之一,然而,手動編寫大型表格可能會費時且容易出錯,本文將介紹如何使用Python自動創(chuàng)建Markdown表格,通過示例代碼詳細展示各種場景下的創(chuàng)建方法,提高表格生成的效率2024-01-01自動在Windows中運行Python腳本并定時觸發(fā)功能實現(xiàn)
講一下在Python中寫好了一個腳本之后,怎么自動雙擊一個程序自動就跑起來。以及,怎么在Windows 10中設計定期定時觸發(fā)并跑腳本,有需要的朋友可以參考下2021-09-09