深入理解Python中裝飾器的用法
因?yàn)楹瘮?shù)或類都是對(duì)象,它們也能被四處傳遞。它們又是可變對(duì)象,可以被更改。在函數(shù)或類對(duì)象創(chuàng)建后但綁定到名字前更改之的行為為裝飾(decorator)。
“裝飾器”后隱藏了兩種意思——一是函數(shù)起了裝飾作用,例如,執(zhí)行真正的工作,另一個(gè)是依附于裝飾器語(yǔ)法的表達(dá)式,例如,at符號(hào)和裝飾函數(shù)的名稱。
函數(shù)可以通過(guò)函數(shù)裝飾器語(yǔ)法裝飾:
@decorator # ② def function(): # ① pass
以@做為定義為裝飾器函數(shù)前綴的表達(dá)式②。在 @ 后的部分必須是簡(jiǎn)單的表達(dá)式,通常只是函數(shù)或類的名字。這一部分先求值,在下面的定義的函數(shù)準(zhǔn)備好后,裝飾器被新定義的函數(shù)對(duì)象作為單個(gè)參數(shù)調(diào)用。裝飾器返回的值附著到被裝飾的函數(shù)名。
裝飾器可以應(yīng)用到函數(shù)和類上。對(duì)類語(yǔ)義很明晰——類定義被當(dāng)作參數(shù)來(lái)調(diào)用裝飾器,無(wú)論返回什么都賦給被裝飾的名字。
def function(): # ① pass function = decorator(function) # ②
裝飾器語(yǔ)法因其可讀性被選擇。因?yàn)檠b飾器在函數(shù)頭部前被指定,顯然不是函數(shù)體的一部分,它只能對(duì)整個(gè)函數(shù)起作用。以@為前綴的表達(dá)式又讓它明顯到不容忽視(根據(jù)PEP叫在您臉上……:))。當(dāng)多個(gè)裝飾器被應(yīng)用時(shí),每個(gè)放在不同的行非常易于閱讀。
代替和調(diào)整原始對(duì)象
裝飾器可以或者返回相同的函數(shù)或類對(duì)象或者返回完全不同的對(duì)象。第一種情況中,裝飾器利用函數(shù)或類對(duì)象是可變的添加屬性,例如向類添加文檔字符串(docstring).裝飾器甚至可以在不改變對(duì)象的情況下做有用的事,例如在全局注冊(cè)表中注冊(cè)裝飾的類。在第二種情況中,簡(jiǎn)直無(wú)所不能:當(dāng)什么不同的東西取代了被裝飾的類或函數(shù),新對(duì)象可以完全不同。然而這不是裝飾器的目的:它們意在改變裝飾對(duì)象而非做不可預(yù)料的事。因此當(dāng)一個(gè)函數(shù)在裝飾時(shí)被完全替代成不同的函數(shù)時(shí),新函數(shù)通常在一些準(zhǔn)備工作后調(diào)用原始函數(shù)。同樣,當(dāng)一個(gè)類被裝飾成一個(gè)新類時(shí),新類通常源于被裝飾類。當(dāng)裝飾器的目的是“每次都”做什么,像記錄每次對(duì)被裝飾函數(shù)的調(diào)用,只有第二類裝飾器可用。另一方面,如果第一類足夠了,最好使用它因?yàn)楦?jiǎn)單。
實(shí)現(xiàn)類和函數(shù)裝飾器
對(duì)裝飾器惟一的要求是它能夠單參數(shù)調(diào)用。這意味著裝飾器可以作為常規(guī)函數(shù)或帶有__call__方法的類的實(shí)現(xiàn),理論上,甚至lambda函數(shù)也行。
讓我們比較函數(shù)和類方法。裝飾器表達(dá)式(@后部分)可以只是名字。只有名字的方法很好(打字少,看起來(lái)整潔等),但是只有當(dāng)無(wú)需用參數(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
這兩個(gè)裝飾器屬于返回被裝飾函數(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ù)會(huì)接受,所以wrapper將所有東西都創(chuàng)遞給被裝飾函數(shù)。一個(gè)不幸的結(jié)果就是顯式參數(shù)很迷惑人。
相比定義為函數(shù)的裝飾器,定義為類的復(fù)雜裝飾器更簡(jiǎn)單。當(dāng)對(duì)象被創(chuàng)建,__init__方法僅僅允許返回None,創(chuàng)建的對(duì)象類型不能更改。這意味著當(dāng)裝飾器被定義為類時(shí),使用無(wú)參數(shù)的形式?jīng)]什么意義:最終被裝飾的對(duì)象只是裝飾類的一個(gè)實(shí)例而已,被構(gòu)建器(constructor)調(diào)用返回,并不非常有用。討論在裝飾表達(dá)式中給出參數(shù)的基于類的裝飾器,__init__方法被用來(lái)構(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, () {}
相對(duì)于正常規(guī)則(PEP 8)由類寫成的裝飾器表現(xiàn)得更像函數(shù),因此它們的名字以小寫字母開始。
事實(shí)上,創(chuàng)建一個(gè)僅返回被裝飾函數(shù)的新類沒(méi)什么意義。對(duì)象應(yīng)該有狀態(tài),這種裝飾器在裝飾器返回新對(duì)象時(shí)更有用。
>>> 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) {}
像這樣的裝飾器可以做任何事,因?yàn)樗芨淖儽谎b飾函數(shù)對(duì)象和參數(shù),調(diào)用被裝飾函數(shù)或不調(diào)用,最后改變返回值。
復(fù)制原始函數(shù)的文檔字符串和其它屬性
當(dāng)新函數(shù)被返回代替裝飾前的函數(shù)時(shí),不幸的是原函數(shù)的函數(shù)名,文檔字符串和參數(shù)列表都丟失了。這些屬性可以部分通過(guò)設(shè)置__doc__(文檔字符串),__module__和__name__(函數(shù)的全稱)、__annotations__(Python 3中關(guān)于參數(shù)和返回值的額外信息)移植到新函數(shù)上,這些工作可通過(guò)functools.update_wrapper自動(dòng)完成。
>>> 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ù)的默認(rèn)值可以通過(guò)__defaults__、__kwdefaults__屬性更改,但是不幸的是參數(shù)列表本身不能被設(shè)置為屬性。這意味著help(function)將顯式無(wú)用的參數(shù)列表,使使用者迷惑不已。一個(gè)解決此問(wèn)題有效但是丑陋的方式是使用eval動(dòng)態(tài)創(chuàng)建wrapper??梢允褂猛獠縠xternal模塊自動(dòng)實(shí)現(xiàn)。它提供了對(duì)decorator裝飾器的支持,該裝飾器接受wrapper并將之轉(zhuǎn)換成保留函數(shù)簽名的裝飾器。
綜上,裝飾器應(yīng)該總是使用functools.update_wrapper或者其它方式賦值函數(shù)屬性。
標(biāo)準(zhǔn)庫(kù)中的示例
首先要提及的是標(biāo)準(zhǔn)庫(kù)中有一些實(shí)用的裝飾器,有三種裝飾器:
classmethod讓一個(gè)方法變成“類方法”,即它能夠無(wú)需創(chuàng)建實(shí)例調(diào)用。當(dāng)一個(gè)常規(guī)方法被調(diào)用時(shí),解釋器插入實(shí)例對(duì)象作為第一個(gè)參數(shù)self。當(dāng)類方法被調(diào)用時(shí),類本身被給做第一個(gè)參數(shù),一般叫cls。
類方法也能通過(guò)類命名空間讀取,所以它們不必污染模塊命名空間。類方法可用來(lái)提供替代的構(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)
這比用一大堆標(biāo)記的__init__簡(jiǎn)單多了。
staticmethod應(yīng)用到方法上讓它們“靜態(tài)”,例如,本來(lái)一個(gè)常規(guī)函數(shù),但通過(guò)類命名空間存取。這在函數(shù)僅在類中需要時(shí)有用(它的名字應(yīng)該以_為前綴),或者當(dāng)我們想要用戶以為方法連接到類時(shí)也有用——雖然對(duì)實(shí)現(xiàn)本身不必要。
property是對(duì)getter和setter問(wèn)題Python風(fēng)格的答案。通過(guò)property裝飾的方法變成在屬性存取時(shí)自動(dòng)調(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使它能夠直接被計(jì)算,并且產(chǎn)生只讀的副作用,因?yàn)闆](méi)有定義任何setter。
為了得到setter和getter,顯然需要兩個(gè)方法。從Python 2.6開始首選以下語(yǔ)法:
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
通過(guò)property裝飾器取代帶一個(gè)屬性(property)對(duì)象的getter方法,以上代碼起作用。這個(gè)對(duì)象反過(guò)來(lái)有三個(gè)可用于裝飾器的方法getter、setter和deleter。它們的作用就是設(shè)定屬性對(duì)象的getter、setter和deleter(被存儲(chǔ)為fget、fset和fdel屬性(attributes))。當(dāng)創(chuàng)建對(duì)象時(shí),getter可以像上例一樣設(shè)定。當(dāng)定義setter時(shí),我們已經(jīng)在area中有property對(duì)象,可以通過(guò)setter方法向它添加setter,一切都在創(chuàng)建類時(shí)完成。
之后,當(dāng)類實(shí)例創(chuàng)建后,property對(duì)象和特殊。當(dāng)解釋器執(zhí)行屬性存取、賦值或刪除時(shí),其執(zhí)行被下放給property對(duì)象的方法。
為了讓一切一清二楚[^5],讓我們定義一個(gè)“調(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)是對(duì)裝飾器語(yǔ)法的一點(diǎn)擴(kuò)展。使用裝飾器的一大前提——命名不重復(fù)——被違反了,但是目前沒(méi)什么更好的發(fā)明。為getter,setter和deleter方法使用相同的名字還是個(gè)好的風(fēng)格。
一些其它更新的例子包括:
functools.lru_cache記憶任意維持有限 參數(shù):結(jié)果 對(duì)的緩存函數(shù)(Python
3.2)
functools.total_ordering是一個(gè)基于單個(gè)比較方法而填充丟失的比較(ordering)方法(__lt__,__gt__,__le__等等)的類裝飾器。
函數(shù)的廢棄
比如說(shuō)我們想在第一次調(diào)用我們不希望被調(diào)用的函數(shù)時(shí)在標(biāo)準(zhǔn)錯(cuò)誤打印一個(gè)廢棄函數(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)
也可以實(shí)現(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移除裝飾器
例如我們有個(gè)返回列表的函數(shù),這個(gè)列表由循環(huán)創(chuàng)建。如果我們不知道需要多少對(duì)象,實(shí)現(xiàn)這個(gè)的標(biāo)準(zhǔ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ù)雜,正如真實(shí)的代碼中發(fā)生的那樣,這就很難讀懂了。我們可以通過(guò)yield語(yǔ)句簡(jiǎn)化它,但之后用戶不得不顯式調(diào)用嗯list(find_answers())。
我們可以創(chuàng)建一個(gè)為我們構(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
插件注冊(cè)系統(tǒng)
這是一個(gè)僅僅把它放進(jìn)全局注冊(cè)表中而不更改類的類裝飾器,它屬于返回被裝飾對(duì)象的裝飾器。
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}')
這里我們使用裝飾器完成插件注冊(cè)。我們通過(guò)一個(gè)名詞調(diào)用裝飾器而不是一個(gè)動(dòng)詞,因?yàn)槲覀冇盟鼇?lái)聲明我們的類是WordProcessor的一個(gè)插件。plugin方法僅僅將類添加進(jìn)插件列表。
關(guān)于插件自身說(shuō)下:它用真正的Unicode中的破折號(hào)符號(hào)替代HTML中的破折號(hào)。它利用unicode literal notation通過(guò)它在unicode數(shù)據(jù)庫(kù)中的名稱(“EM DASH”)插入一個(gè)符號(hào)。如果直接插入U(xiǎn)nicode符號(hào),將不可能區(qū)分所插入的和源程序中的破折號(hào)。
相關(guān)文章
python自動(dòng)發(fā)微信監(jiān)控報(bào)警
這篇文章主要為大家詳細(xì)介紹了python自動(dòng)發(fā)微信監(jiān)控報(bào)警,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09使用實(shí)現(xiàn)pandas讀取csv文件指定的前幾行
下面小編就為大家分享一篇使用實(shí)現(xiàn)pandas讀取csv文件指定的前幾行,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-04-04聊聊Python對(duì)CSV文件的讀取與寫入問(wèn)題
今天抽空給大家介紹下Python對(duì)CSV文件的讀取與寫入問(wèn)題,首先需要在python環(huán)境里導(dǎo)入csv板塊,下面就通過(guò)實(shí)例代碼給大家詳細(xì)介紹下,感興趣的朋友跟隨小編一起看看吧2021-11-11Python調(diào)試神器之PySnooper的使用教程分享
對(duì)于每個(gè)程序開發(fā)者來(lái)說(shuō),調(diào)試幾乎是必備技能。本文小編就來(lái)給大家介紹一款非常好用的調(diào)試工具,它能在一些場(chǎng)景下,大幅度提高調(diào)試的效率, 那就是 PySnooper,希望大家喜歡2023-02-02Python自動(dòng)創(chuàng)建Markdown表格使用實(shí)例探究
Markdown表格是文檔中整理和展示數(shù)據(jù)的重要方式之一,然而,手動(dòng)編寫大型表格可能會(huì)費(fèi)時(shí)且容易出錯(cuò),本文將介紹如何使用Python自動(dòng)創(chuàng)建Markdown表格,通過(guò)示例代碼詳細(xì)展示各種場(chǎng)景下的創(chuàng)建方法,提高表格生成的效率2024-01-01自動(dòng)在Windows中運(yùn)行Python腳本并定時(shí)觸發(fā)功能實(shí)現(xiàn)
講一下在Python中寫好了一個(gè)腳本之后,怎么自動(dòng)雙擊一個(gè)程序自動(dòng)就跑起來(lái)。以及,怎么在Windows 10中設(shè)計(jì)定期定時(shí)觸發(fā)并跑腳本,有需要的朋友可以參考下2021-09-09