因?yàn)楹瘮?shù)或類都是對象,它們也能被四處傳遞。它們又是可變對象,可以被更改。在函數(shù)或類對象創(chuàng)建后但綁定到名字前更改之的行為為裝飾(decorator)。
“裝飾器”后隱藏了兩種意思――一是函數(shù)起了裝飾作用,例如,執(zhí)行真正的工作,另一個(gè)是依附于裝飾器語法的表達(dá)式,例如,at符號和裝飾函數(shù)的名稱。
函數(shù)可以通過函數(shù)裝飾器語法裝飾:
@decorator # ②def function(): # ① pass
def function(): # ① passfunction = decorator(function) # ②
裝飾器語法因其可讀性被選擇。因?yàn)檠b飾器在函數(shù)頭部前被指定,顯然不是函數(shù)體的一部分,它只能對整個(gè)函數(shù)起作用。以@為前綴的表達(dá)式又讓它明顯到不容忽視(根據(jù)PEP叫在您臉上……:))。當(dāng)多個(gè)裝飾器被應(yīng)用時(shí),每個(gè)放在不同的行非常易于閱讀。
代替和調(diào)整原始對象
裝飾器可以或者返回相同的函數(shù)或類對象或者返回完全不同的對象。第一種情況中,裝飾器利用函數(shù)或類對象是可變的添加屬性,例如向類添加文檔字符串(docstring).裝飾器甚至可以在不改變對象的情況下做有用的事,例如在全局注冊表中注冊裝飾的類。在第二種情況中,簡直無所不能:當(dāng)什么不同的東西取代了被裝飾的類或函數(shù),新對象可以完全不同。然而這不是裝飾器的目的:它們意在改變裝飾對象而非做不可預(yù)料的事。因此當(dāng)一個(gè)函數(shù)在裝飾時(shí)被完全替代成不同的函數(shù)時(shí),新函數(shù)通常在一些準(zhǔn)備工作后調(diào)用原始函數(shù)。同樣,當(dāng)一個(gè)類被裝飾成一個(gè)新類時(shí),新類通常源于被裝飾類。當(dāng)裝飾器的目的是“每次都”做什么,像記錄每次對被裝飾函數(shù)的調(diào)用,只有第二類裝飾器可用。另一方面,如果第一類足夠了,最好使用它因?yàn)楦唵巍?/p>
實(shí)現(xiàn)類和函數(shù)裝飾器
對裝飾器惟一的要求是它能夠單參數(shù)調(diào)用。這意味著裝飾器可以作為常規(guī)函數(shù)或帶有__call__方法的類的實(shí)現(xiàn),理論上,甚至lambda函數(shù)也行。
讓我們比較函數(shù)和類方法。裝飾器表達(dá)式(@后部分)可以只是名字。只有名字的方法很好(打字少,看起來整潔等),但是只有當(dāng)無需用參數(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 decoratordoing 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 14defining the decoratordoing 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ù)。一個(gè)不幸的結(jié)果就是顯式參數(shù)很迷惑人。
相比定義為函數(shù)的裝飾器,定義為類的復(fù)雜裝飾器更簡單。當(dāng)對象被創(chuàng)建,__init__方法僅僅允許返回None,創(chuàng)建的對象類型不能更改。這意味著當(dāng)裝飾器被定義為類時(shí),使用無參數(shù)的形式?jīng)]什么意義:最終被裝飾的對象只是裝飾類的一個(gè)實(shí)例而已,被構(gòu)建器(constructor)調(diào)用返回,并不非常有用。討論在裝飾表達(dá)式中給出參數(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, kwargsin decorator call, foo>>> function()in function, () {}相對于正常規(guī)則(PEP 8)由類寫成的裝飾器表現(xiàn)得更像函數(shù),因此它們的名字以小寫字母開始。
事實(shí)上,創(chuàng)建一個(gè)僅返回被裝飾函數(shù)的新類沒什么意義。對象應(yīng)該有狀態(tài),這種裝飾器在裝飾器返回新對象時(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, kwargsin decorator call, foo>>> function(11, 12)in the wrapper, (11, 12) {}in function, (11, 12) {}像這樣的裝飾器可以做任何事,因?yàn)樗芨淖儽谎b飾函數(shù)對象和參數(shù),調(diào)用被裝飾函數(shù)或不調(diào)用,最后改變返回值。
復(fù)制原始函數(shù)的文檔字符串和其它屬性
當(dāng)新函數(shù)被返回代替裝飾前的函數(shù)時(shí),不幸的是原函數(shù)的函數(shù)名,文檔字符串和參數(shù)列表都丟失了。這些屬性可以部分通過設(shè)置__doc__(文檔字符串),__module__和__name__(函數(shù)的全稱)、__annotations__(Python 3中關(guān)于參數(shù)和返回值的額外信息)移植到新函數(shù)上,這些工作可通過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 14defining the decoratordoing decoration, abc>>> function <function function at 0x...>>>> print function.__doc__extensive documentation一件重要的東西是從可遷移屬性列表中所缺少的:參數(shù)列表。參數(shù)的默認(rèn)值可以通過__defaults__、__kwdefaults__屬性更改,但是不幸的是參數(shù)列表本身不能被設(shè)置為屬性。這意味著help(function)將顯式無用的參數(shù)列表,使使用者迷惑不已。一個(gè)解決此問題有效但是丑陋的方式是使用eval動(dòng)態(tài)創(chuàng)建wrapper。可以使用外部external模塊自動(dòng)實(shí)現(xiàn)。它提供了對decorator裝飾器的支持,該裝飾器接受wrapper并將之轉(zhuǎn)換成保留函數(shù)簽名的裝飾器。
綜上,裝飾器應(yīng)該總是使用functools.update_wrapper或者其它方式賦值函數(shù)屬性。
標(biāo)準(zhǔn)庫中的示例
首先要提及的是標(biāo)準(zhǔn)庫中有一些實(shí)用的裝飾器,有三種裝飾器:
classmethod讓一個(gè)方法變成“類方法”,即它能夠無需創(chuàng)建實(shí)例調(diào)用。當(dāng)一個(gè)常規(guī)方法被調(diào)用時(shí),解釋器插入實(shí)例對象作為第一個(gè)參數(shù)self。當(dāng)類方法被調(diào)用時(shí),類本身被給做第一個(gè)參數(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)
這比用一大堆標(biāo)記的__init__簡單多了。
staticmethod應(yīng)用到方法上讓它們“靜態(tài)”,例如,本來一個(gè)常規(guī)函數(shù),但通過類命名空間存取。這在函數(shù)僅在類中需要時(shí)有用(它的名字應(yīng)該以_為前綴),或者當(dāng)我們想要用戶以為方法連接到類時(shí)也有用――雖然對實(shí)現(xiàn)本身不必要。
property是對getter和setter問題Python風(fēng)格的答案。通過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)闆]有定義任何setter。
為了得到setter和getter,顯然需要兩個(gè)方法。從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裝飾器取代帶一個(gè)屬性(property)對象的getter方法,以上代碼起作用。這個(gè)對象反過來有三個(gè)可用于裝飾器的方法getter、setter和deleter。它們的作用就是設(shè)定屬性對象的getter、setter和deleter(被存儲為fget、fset和fdel屬性(attributes))。當(dāng)創(chuàng)建對象時(shí),getter可以像上例一樣設(shè)定。當(dāng)定義setter時(shí),我們已經(jīng)在area中有property對象,可以通過setter方法向它添加setter,一切都在創(chuàng)建類時(shí)完成。
之后,當(dāng)類實(shí)例創(chuàng)建后,property對象和特殊。當(dāng)解釋器執(zhí)行屬性存取、賦值或刪除時(shí),其執(zhí)行被下放給property對象的方法。
為了讓一切一清二楚[^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.agetting 11>>> d.a = 2setting 2>>> del d.adeleting>>> d.agetting 11
屬性(property)是對裝飾器語法的一點(diǎn)擴(kuò)展。使用裝飾器的一大前提――命名不重復(fù)――被違反了,但是目前沒什么更好的發(fā)明。為getter,setter和deleter方法使用相同的名字還是個(gè)好的風(fēng)格。
一些其它更新的例子包括:
functools.lru_cache記憶任意維持有限 參數(shù):結(jié)果 對的緩存函數(shù)(Python
3.2)
functools.total_ordering是一個(gè)基于單個(gè)比較方法而填充丟失的比較(ordering)方法(__lt__,__gt__,__le__等等)的類裝飾器。
函數(shù)的廢棄
比如說我們想在第一次調(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)建。如果我們不知道需要多少對象,實(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ā)生的那樣,這就很難讀懂了。我們可以通過yield語句簡化它,但之后用戶不得不顯式調(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ù)變成這樣:
@vectorizeddef find_answers(): while True: ans = look_for_next_answer() if ans is None: break yield ans
插件注冊系統(tǒng)
這是一個(gè)僅僅把它放進(jìn)全局注冊表中而不更改類的類裝飾器,它屬于返回被裝飾對象的裝飾器。
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}')這里我們使用裝飾器完成插件注冊。我們通過一個(gè)名詞調(diào)用裝飾器而不是一個(gè)動(dòng)詞,因?yàn)槲覀冇盟鼇砺暶魑覀兊念愂荳ordProcessor的一個(gè)插件。plugin方法僅僅將類添加進(jìn)插件列表。
關(guān)于插件自身說下:它用真正的Unicode中的破折號符號替代HTML中的破折號。它利用unicode literal notation通過它在unicode數(shù)據(jù)庫中的名稱(“EM DASH”)插入一個(gè)符號。如果直接插入U(xiǎn)nicode符號,將不可能區(qū)分所插入的和源程序中的破折號。
新聞熱點(diǎn)
疑難解答
圖片精選