這章有關(guān)Python中被認(rèn)為高級的特性——就是說并不是每個語言都有的,也是說它們可能在更復(fù)雜的程序或庫中更有用,但不是說特別特殊或特別復(fù)雜。
強調(diào)這點很重要:這一章僅僅關(guān)于語言自身——關(guān)于輔之以Python的標(biāo)準(zhǔn)庫功能的特殊語法所支持的特性,不包括那些智能的外部模塊實現(xiàn)。
在開發(fā)Python程序語言的過程中,它的語法,獨一無二。因為它非常透明。建議的更改通過不同的角度評估并在公開郵件列表討論,最終決定考慮到假設(shè)用例的重要性、添加更多特性的負(fù)擔(dān),其余語法的一致性、是否建議的變種易于讀寫和理解之間的平衡。這個過程由Python Enhancement Proposals(PEPs)的形式規(guī)范。最終這一章節(jié)中描述的特性在證明它們確實解決實際問題并且使用起來盡可能簡單后被添加。
迭代器(Iterators), 生成表達式(generator expressions)和生成器(generators)
迭代器
簡單
重復(fù)工作是浪費,將不同“土生土長”的方法替換為標(biāo)準(zhǔn)特性換來的是更加易于閱讀和操作。
Guido van Rossum — Adding Optional Static Typing to Python
迭代器是依附于迭代協(xié)議的對象——基本意味它有一個next 方法(method),當(dāng)調(diào)用時,返回序列中的下一個項目。當(dāng)無項目可返回時,引發(fā)(raise)StopIteration 異常。
迭代對象允許一次循環(huán)。它保留單次迭代的狀態(tài)(位置),或從另一個角度講,每次循環(huán)序列都需要一個迭代對象。這意味我們可以同時迭代同一個序列不只一次。將迭代邏輯和序列分離使我們有更多的迭代方式。
調(diào)用一個容器(container)的__iter__ 方法創(chuàng)建迭代對象是掌握迭代器最直接的方式。iter 函數(shù)為我們節(jié)約一些按鍵。
>>> nums = [1,2,3] # note that ... varies: these are different objects
>>> iter(nums)
<listiterator object at ...>
>>> nums.__iter__()
<listiterator object at ...>
>>> nums.__reversed__()
<listreverseiterator object at ...>
>>> it = iter(nums)
>>> next(it) # next(obj) simply calls obj.next()
1
>>> it.next()
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
當(dāng)在循環(huán)中使用時,StopIteration 被接受并停止循環(huán)。但通過顯式引發(fā)(invocation),我們看到一旦迭代器元素被耗盡,存取它將引發(fā)異常。
使用for...in 循環(huán)也使用__iter__ 方法。這允許我們透明地開始對一個序列迭代。但是如果我們已經(jīng)有一個迭代器,我們想在for循環(huán)中能同樣地使用它們。為了實現(xiàn)這點,迭代器除了next 還有一個方法__iter__ 來返回迭代器自身(self)。
Python中對迭代器的支持無處不在:標(biāo)準(zhǔn)庫中的所有序列和無序容器都支持。這個概念也被拓展到其它東西:例如file 對象支持行的迭代。
>>> f = open('/etc/fstab')
>>> f is f.__iter__()
True
file 自身就是迭代器,它的__iter__ 方法并不創(chuàng)建一個單獨的對象:僅僅單線程的順序讀取被允許。
生成表達式
第二種創(chuàng)建迭代對象的方式是通過 生成表達式(generator expression) ,列表推導(dǎo)(list comprehension)的基礎(chǔ)。為了增加清晰度,生成表達式總是封裝在括號或表達式中。如果使用圓括號,則創(chuàng)建了一個生成迭代器(generator iterator)。如果是方括號,這一過程被‘短路’我們獲得一個列表list 。
>>> (i for i in nums)
<generator object <genexpr> at 0x...>
>>> [i for i in nums]
[1, 2, 3]
>>> list(i for i in nums)
[1, 2, 3]
在Python 2.7和 3.x中列表表達式語法被擴展到 字典和集合表達式。一個集合set 當(dāng)生成表達式是被大括號封裝時被創(chuàng)建。一個字典dict 在表達式包含key:value 形式的鍵值對時被創(chuàng)建:
>>> {i for i in range(3)}
set([0, 1, 2])
>>> {i:i**2 for i in range(3)}
{0: 0, 1: 1, 2: 4}
如果您不幸身陷古老的Python版本中,這個語法有點糟:
>>> set(i for i in 'abc')
set(['a', 'c', 'b'])
>>> dict((i, ord(i)) for i in 'abc')
{'a': 97, 'c': 99, 'b': 98}
生成表達式相當(dāng)簡單,不用多說。只有一個陷阱值得提及:在版本小于3的Python中索引變量(i )會泄漏。
生成器
生成器
生成器是產(chǎn)生一列結(jié)果而不是單一值的函數(shù)。
David Beazley — A Curious Course on Coroutines and Concurrency
第三種創(chuàng)建迭代對象的方式是調(diào)用生成器函數(shù)。一個 生成器(generator) 是包含關(guān)鍵字yield 的函數(shù)。值得注意,僅僅是這個關(guān)鍵字的出現(xiàn)完全改變了函數(shù)的本質(zhì):yield 語句不必引發(fā)(invoke),甚至不必可接觸。但讓函數(shù)變成了生成器。當(dāng)一個函數(shù)被調(diào)用時,其中的指令被執(zhí)行。而當(dāng)一個生成器被調(diào)用時,執(zhí)行在其中第一條指令之前停止。生成器的調(diào)用創(chuàng)建依附于迭代協(xié)議的生成器對象。就像常規(guī)函數(shù)一樣,允許并發(fā)和遞歸調(diào)用。
當(dāng)next 被調(diào)用時,函數(shù)執(zhí)行到第一個yield 。每次遇到yield 語句獲得一個作為next 返回的值,在yield 語句執(zhí)行后,函數(shù)的執(zhí)行又被停止。
>>> def f():
... yield 1
... yield 2
>>> f()
<generator object f at 0x...>
>>> gen = f()
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
讓我們遍歷單個生成器函數(shù)調(diào)用的整個歷程。
>>> def f():
... print("-- start --")
... yield 3
... print("-- middle --")
... yield 4
... print("-- finished --")
>>> gen = f()
>>> next(gen)
-- start --
3
>>> next(gen)
-- middle --
4
>>> next(gen)
-- finished --
Traceback (most recent call last):
...
StopIteration
相比常規(guī)函數(shù)中執(zhí)行f() 立即讓print 執(zhí)行,gen 不執(zhí)行任何函數(shù)體中語句就被賦值。只有當(dāng)gen.next() 被next 調(diào)用,直到第一個yield 部分的語句才被執(zhí)行。第二個語句打印-- middle -- 并在遇到第二個yield時停止執(zhí)行。第三個next 打印-- finished -- 并且到函數(shù)末尾,因為沒有yield ,引發(fā)了異常。
當(dāng)函數(shù)yield之后控制返回給調(diào)用者后發(fā)生了什么?每個生成器的狀態(tài)被存儲在生成器對象中。從這點看生成器函數(shù),好像它是運行在單獨的線程,但這僅僅是假象:執(zhí)行是嚴(yán)格單線程的,但解釋器保留和存儲在下一個值請求之間的狀態(tài)。
為何生成器有用?正如關(guān)于迭代器這部分強調(diào)的,生成器函數(shù)只是創(chuàng)建迭代對象的又一種方式。一切能被yield 語句完成的東西也能被next 方法完成。然而,使用函數(shù)讓解釋器魔力般地創(chuàng)建迭代器有優(yōu)勢。一個函數(shù)可以比需要next 和__iter__ 方法的類定義短很多。更重要的是,相比不得不對迭代對象在連續(xù)next 調(diào)用之間傳遞的實例(instance)屬性來說,生成器的作者能更簡單的理解局限在局部變量中的語句。
還有問題是為何迭代器有用?當(dāng)一個迭代器用來驅(qū)動循環(huán),循環(huán)變得簡單。迭代器代碼初始化狀態(tài),決定是否循環(huán)結(jié)束,并且找到下一個被提取到不同地方的值。這凸顯了循環(huán)體——最值得關(guān)注的部分。除此之外,可以在其它地方重用迭代器代碼。
雙向通信
每個yield 語句將一個值傳遞給調(diào)用者。這就是為何PEP 255引入生成器(在Python2.2中實現(xiàn))。但是相反方向的通信也很有用。一個明顯的方式是一些外部(extern)語句,或者全局變量或共享可變對象。通過將先前無聊的yield 語句變成表達式,直接通信因PEP 342成為現(xiàn)實(在2.5中實現(xiàn))。當(dāng)生成器在yield語句之后恢復(fù)執(zhí)行時,調(diào)用者可以對生成器對象調(diào)用一個方法,或者傳遞一個值 給 生成器,然后通過yield 語句返回,或者通過一個不同的方法向生成器注入異常。
第一個新方法是send(value) ,類似于next() ,但是將value 傳遞進作為yield表達式值的生成器中。事實上,g.next() 和g.send(None) 是等效的。
第二個新方法是throw(type, value=None, traceback=None) ,等效于在yield語句處
raise type, value, traceback
不像raise (從執(zhí)行點立即引發(fā)異常),throw() 首先恢復(fù)生成器,然后僅僅引發(fā)異常。選用單次throw就是因為它意味著把異常放到其它位置,并且在其它語言中與異常有關(guān)。
當(dāng)生成器中的異常被引發(fā)時發(fā)生什么?它可以或者顯式引發(fā),當(dāng)執(zhí)行某些語句時可以通過throw() 方法注入到y(tǒng)ield語句中。任一情況中,異常都以標(biāo)準(zhǔn)方式傳播:它可以被except 和finally 捕獲,或者造成生成器的中止并傳遞給調(diào)用者。
因完整性緣故,值得提及生成器迭代器也有close() 方法,該方法被用來讓本可以提供更多值的生成器立即中止。它用生成器的__del__ 方法銷毀保留生成器狀態(tài)的對象。
讓我們定義一個只打印出通過send和throw方法所傳遞東西的生成器。
>>> import itertools
>>> def g():
... print '--start--'
... for i in itertools.count():
... print '--yielding %i--' % i
... try:
... ans = yield i
... except GeneratorExit:
... print '--closing--'
... raise
... except Exception as e:
... print '--yield raised %r--' % e
... else:
... print '--yield returned %s--' % ans
>>> it = g()
>>> next(it)
--start--
--yielding 0--
0
>>> it.send(11)
--yield returned 11--
--yielding 1--
1
>>> it.throw(IndexError)
--yield raised IndexError()--
--yielding 2--
2
>>> it.close()
--closing--
注意: next 還是__next__ ?
在Python 2.x中,接受下一個值的迭代器方法是next ,它通過全局函數(shù)next 顯式調(diào)用,意即它應(yīng)該調(diào)用__next__ 。就像全局函數(shù)iter 調(diào)用__iter__ 。這種不一致在Python 3.x中被修復(fù),it.next 變成了it.__next__ 。對于其它生成器方法——send 和throw 情況更加復(fù)雜,因為它們不被解釋器隱式調(diào)用。然而,有建議語法擴展讓continue 帶一個將被傳遞給循環(huán)迭代器中send 的參數(shù)。如果這個擴展被接受,可能gen.send 會變成gen.__send__ 。最后一個生成器方法close 顯然被不正確的命名了,因為它已經(jīng)被隱式調(diào)用。
鏈?zhǔn)缴善?/h3>
注意: 這是PEP 380的預(yù)覽(還未被實現(xiàn),但已經(jīng)被Python3.3接受)
比如說我們正寫一個生成器,我們想要yield一個第二個生成器——一個子生成器(subgenerator)——生成的數(shù)。如果僅考慮產(chǎn)生(yield)的值,通過循環(huán)可以不費力的完成:
subgen = some_other_generator()
for v in subgen:
yield v
然而,如果子生成器需要調(diào)用send() 、throw() 和close() 和調(diào)用者適當(dāng)交互的情況下,事情就復(fù)雜了。yield 語句不得不通過類似于前一章節(jié)部分定義的try...except...finally 結(jié)構(gòu)來保證“調(diào)試”生成器函數(shù)。這種代碼在PEP 380中提供,現(xiàn)在足夠拿出將在Python 3.3中引入的新語法了:
yield from some_other_generator()
像上面的顯式循環(huán)調(diào)用一樣,重復(fù)從some_other_generator 中產(chǎn)生值直到?jīng)]有值可以產(chǎn)生,但是仍然向子生成器轉(zhuǎn)發(fā)send 、throw 和close 。
裝飾器
總結(jié)
這個語言中令人激動的特性幾乎充滿歉意的,考慮到它可能沒這么有用。
Bruce Eckel — An Introduction to Python Decorators
因為函數(shù)或類都是對象,它們也能被四處傳遞。它們又是可變對象,可以被更改。在函數(shù)或類對象創(chuàng)建后但綁定到名字前更改之的行為為裝飾(decorator)。
“裝飾器”后隱藏了兩種意思——一是函數(shù)起了裝飾作用,例如,執(zhí)行真正的工作,另一個是依附于裝飾器語法的表達式,例如,at符號和裝飾函數(shù)的名稱。
函數(shù)可以通過函數(shù)裝飾器語法裝飾:
@decorator # ②
def function(): # ①
pass
- 函數(shù)以標(biāo)準(zhǔn)方式定義。①
- 以
@ 做為定義為裝飾器函數(shù)前綴的表達式②。在 @ 后的部分必須是簡單的表達式,通常只是函數(shù)或類的名字。這一部分先求值,在下面的定義的函數(shù)準(zhǔn)備好后,裝飾器被新定義的函數(shù)對象作為單個參數(shù)調(diào)用。裝飾器返回的值附著到被裝飾的函數(shù)名。
裝飾器可以應(yīng)用到函數(shù)和類上。對類語義很明晰——類定義被當(dāng)作參數(shù)來調(diào)用裝飾器,無論返回什么都賦給被裝飾的名字。
在裝飾器語法實現(xiàn)前(PEP 318),通過將函數(shù)和類對象賦給臨時變量然后顯式調(diào)用裝飾器然后將返回值賦給函數(shù)名,可以完成同樣的事。這似乎要打更多的字,也確實裝飾器函數(shù)名用了兩次同時臨時變量要用至少三次,很容易出錯。以上實例相當(dāng)于:
def function(): # ①
pass
function = decorator(function) # ②
裝飾器可以堆棧(stacked)——應(yīng)用的順序是從底到上或從里到外。就是說最初的函數(shù)被當(dāng)作第一次參數(shù)器的參數(shù),無論返回什么都被作為第二個裝飾器的參數(shù)……無論最后一個裝飾器返回什么都被依附到最初函數(shù)的名下。
裝飾器語法因其可讀性被選擇。因為裝飾器在函數(shù)頭部前被指定,顯然不是函數(shù)體的一部分,它只能對整個函數(shù)起作用。以@為前綴的表達式又讓它明顯到不容忽視(根據(jù)PEP叫在您臉上……:))。當(dāng)多個裝飾器被應(yīng)用時,每個放在不同的行非常易于閱讀。
代替和調(diào)整原始對象
裝飾器可以或者返回相同的函數(shù)或類對象或者返回完全不同的對象。第一種情況中,裝飾器利用函數(shù)或類對象是可變的添加屬性,例如向類添加文檔字符串(docstring).裝飾器甚至可以在不改變對象的情況下做有用的事,例如在全局注冊表中注冊裝飾的類。在第二種情況中,簡直無所不能:當(dāng)什么不同的東西取代了被裝飾的類或函數(shù),新對象可以完全不同。然而這不是裝飾器的目的:它們意在改變裝飾對象而非做不可預(yù)料的事。因此當(dāng)一個函數(shù)在裝飾時被完全替代成不同的函數(shù)時,新函數(shù)通常在一些準(zhǔn)備工作后調(diào)用原始函數(shù)。同樣,當(dāng)一個類被裝飾成一個新類時,新類通常源于被裝飾類。當(dāng)裝飾器的目的是“每次都”做什么,像記錄每次對被裝飾函數(shù)的調(diào)用,只有第二類裝飾器可用。另一方面,如果第一類足夠了,最好使用它因為更簡單。
實現(xiàn)類和函數(shù)裝飾器
對裝飾器惟一的要求是它能夠單參數(shù)調(diào)用。這意味著裝飾器可以作為常規(guī)函數(shù)或帶有__call__ 方法的類的實現(xiàn),理論上,甚至lambda函數(shù)也行。
讓我們比較函數(shù)和類方法。裝飾器表達式(@后部分)可以只是名字。只有名字的方法很好(打字少,看起來整潔等),但是只有當(dāng)無需用參數(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ù)的裝飾器,定義為類的復(fù)雜裝飾器更簡單。當(dāng)對象被創(chuàng)建,__init__ 方法僅僅允許返回None ,創(chuàng)建的對象類型不能更改。這意味著當(dā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ù)的新類沒什么意義。對象應(yīng)該有狀態(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)用,最后改變返回值。
復(fù)制原始函數(shù)的文檔字符串和其它屬性
當(dāng)新函數(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ù)的默認(rèn)值可以通過__defaults__ 、__kwdefaults__ 屬性更改,但是不幸的是參數(shù)列表本身不能被設(shè)置為屬性。這意味著help(function) 將顯式無用的參數(shù)列表,使使用者迷惑不已。一個解決此問題有效但是丑陋的方式是使用eval 動態(tài)創(chuàng)建wrapper??梢允褂猛獠?code>external模塊自動實現(xiàn)。它提供了對decorator 裝飾器的支持,該裝飾器接受wrapper并將之轉(zhuǎn)換成保留函數(shù)簽名的裝飾器。
綜上,裝飾器應(yīng)該總是使用functools.update_wrapper 或者其它方式賦值函數(shù)屬性。
標(biāo)準(zhǔn)庫中的示例
首先要提及的是標(biāo)準(zhǔn)庫中有一些實用的裝飾器,有三種裝飾器:
-
classmethod 讓一個方法變成“類方法”,即它能夠無需創(chuàng)建實例調(diào)用。當(dāng)一個常規(guī)方法被調(diào)用時,解釋器插入實例對象作為第一個參數(shù)self 。當(dāng)類方法被調(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)
這比用一大堆標(biāo)記的__init__ 簡單多了。
staticmethod 應(yīng)用到方法上讓它們“靜態(tài)”,例如,本來一個常規(guī)函數(shù),但通過類命名空間存取。這在函數(shù)僅在類中需要時有用(它的名字應(yīng)該以_ 為前綴),或者當(dāng)我們想要用戶以為方法連接到類時也有用——雖然對實現(xiàn)本身不必要。
-
property 是對getter和setter問題Python風(fēng)格的答案。通過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 。它們的作用就是設(shè)定屬性對象的getter、setter和deleter(被存儲為fget 、fset 和fdel 屬性(attributes))。當(dāng)創(chuàng)建對象時,getter可以像上例一樣設(shè)定。當(dāng)定義setter時,我們已經(jīng)在area 中有property對象,可以通過setter 方法向它添加setter,一切都在創(chuàng)建類時完成。
之后,當(dāng)類實例創(chuàng)建后,property對象和特殊。當(dāng)解釋器執(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ù)——被違反了,但是目前沒什么更好的發(fā)明。為getter,setter和deleter方法使用相同的名字還是個好的風(fēng)格。
一些其它更新的例子包括:
-
functools.lru_cache 記憶任意維持有限 參數(shù):結(jié)果 對的緩存函數(shù)(Python
3.2) -
functools.total_ordering 是一個基于單個比較方法而填充丟失的比較(ordering)方法(__lt__ ,__gt__ ,__le__ 等等)的類裝飾器。
函數(shù)的廢棄
比如說我們想在第一次調(diào)用我們不希望被調(diào)用的函數(shù)時在標(biāo)準(zhǔn)錯誤打印一個廢棄函數(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)這個的標(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ù)雜,正如真實的代碼中發(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ū)分所插入的和源程序中的破折號。
更多例子和參考
上下文管理器
上下文管理器是可以在with 語句中使用,擁有__enter__ 和__exit__ 方法的對象。
with manager as var:
do_something(var)
相當(dāng)于以下情況的簡化:
var = manager.__enter__()
try:
do_something(var)
finally:
manager.__exit__()
換言之,PEP 343中定義的上下文管理器協(xié)議允許將無聊的try...except...finally 結(jié)構(gòu)抽象到一個單獨的類中,僅僅留下關(guān)注的do_something 部分。
-
__enter__ 方法首先被調(diào)用。它可以返回賦給var 的值。as 部分是可選的:如果它不出現(xiàn),enter 的返回值簡單地被忽略。 -
with 語句下的代碼被執(zhí)行。就像try 子句,它們或者成功執(zhí)行到底,或者break ,continue 或return ,或者可以拋出異常。無論哪種情況,該塊結(jié)束后,__exit__ 方法被調(diào)用。如果拋出異常,異常信息被傳遞給__exit__ ,這將在下一章節(jié)討論。通常情況下,異??杀缓雎?,就像在finally 子句中一樣,并且將在__exit__ 結(jié)束后重新拋出。
比如說我們想確認(rèn)一個文件在完成寫操作之后被立即關(guān)閉:
>>> class closing(object):
... def __init__(self, obj):
... self.obj = obj
... def __enter__(self):
... return self.obj
... def __exit__(self, *args):
... self.obj.close()
>>> with closing(open('/tmp/file', 'w')) as f:
... f.write('the contents\n')
這里我們確保了當(dāng)with 塊退出時調(diào)用了f.close() 。因為關(guān)閉文件是非常常見的操作,該支持已經(jīng)出現(xiàn)在file 類之中。它有一個__exit__ 方法調(diào)用close ,并且本身可作為上下文管理器。
>>> with open('/tmp/file', 'a') as f:
... f.write('more contents\n')
try...finally 常見的用法是釋放資源。各種不同的情況實現(xiàn)相似:在__enter__ 階段資源被獲得,在__exit__ 階段釋放,如果拋出異常也被傳遞。正如文件操作,往往這是對象使用后的自然操作,內(nèi)置支持使之很方便。每一個版本,Python都在更多的地方提供支持。
- 所有類似文件的對象:
-
file ? 自動關(guān)閉 -
fileinput ,tempfile (py >= 3.2) -
bz2.BZ2File ,gzip.GzipFile ,
tarfile.TarFile ,zipfile.ZipFile
-
ftplib , nntplib ? 關(guān)閉連接(py >= 3.2)
- 鎖
-
multiprocessing.RLock ? 鎖定和解鎖 multiprocessing.Semaphore -
memoryview ? 自動釋放(py >= 3.2 或 3.3)
-
decimal.localcontext ? 暫時更改計算精度 -
_winreg.PyHKEY ? 打開和關(guān)閉Hive Key -
warnings.catch_warnings ? 暫時殺死(kill)警告 -
contextlib.closing ? 如上例,調(diào)用close
- 并行編程
-
concurrent.futures.ThreadPoolExecutor ?
并行調(diào)用然后殺掉線程池(py >= 3.2) -
concurrent.futures.ProcessPoolExecutor ?
并行調(diào)用并殺死進程池(py >= 3.2) -
nogil ? 暫時解決GIL問題(僅僅cyphon :()
捕獲異常
當(dāng)一個異常在with 塊中拋出時,它作為參數(shù)傳遞給__exit__ 。三個參數(shù)被使用,和sys.exc_info() 返回的相同:類型、值和回溯(traceback)。當(dāng)沒有異常拋出時,三個參數(shù)都是None 。上下文管理器可以通過從__exit__ 返回一個真(True)值來“吞下”異常。例外可以輕易忽略,因為如果__exit__ 不使用return 直接結(jié)束,返回None ——一個假(False)值,之后在__exit__ 結(jié)束后重新拋出。
捕獲異常的能力創(chuàng)造了有意思的可能性。一個來自單元測試的經(jīng)典例子——我們想確保一些代碼拋出正確種類的異常:
class assert_raises(object):
# based on pytest and unittest.TestCase
def __init__(self, type):
self.type = type
def __enter__(self):
pass
def __exit__(self, type, value, traceback):
if type is None:
raise AssertionError('exception expected')
if issubclass(type, self.type):
return True # swallow the expected exception
raise AssertionError('wrong exception type')
with assert_raises(KeyError):
{}['foo']
使用生成器定義上下文管理器
當(dāng)討論生成器時,據(jù)說我們相比實現(xiàn)為類的迭代器更傾向于生成器,因為它們更短小方便,狀態(tài)被局部保存而非實例和變量中。另一方面,正如雙向通信章節(jié)描述的那樣,生成器和它的調(diào)用者之間的數(shù)據(jù)流可以是雙向的。包括異常,可以直接傳遞給生成器。我們想將上下文管理器實現(xiàn)為特殊的生成器函數(shù)。事實上,生成器協(xié)議被設(shè)計成支持這個用例。
@contextlib.contextmanager
def some_generator(<arguments>):
<setup>
try:
yield <value>
finally:
<cleanup>
contextlib.contextmanager 裝飾一個生成器并轉(zhuǎn)換為上下文管理器。生成器必須遵循一些被包裝(wrapper)函數(shù)強制執(zhí)行的法則——最重要的是它至少yield 一次。yield 之前的部分從__enter__ 執(zhí)行,上下文管理器中的代碼塊當(dāng)生成器停在yield 時執(zhí)行,剩下的在__exit__ 中執(zhí)行。如果異常被拋出,解釋器通過__exit__ 的參數(shù)將之傳遞給包裝函數(shù),包裝函數(shù)于是在yield語句處拋出異常。通過使用生成器,上下文管理器變得更短小精煉。
讓我們用生成器重寫closing 的例子:
@contextlib.contextmanager
def closing(obj):
try:
yield obj
finally:
obj.close()
再把assert_raises 改寫成生成器:
@contextlib.contextmanager
def assert_raises(type):
try:
yield
except type:
return
except Exception as value:
raise AssertionError('wrong exception type')
else:
raise AssertionError('exception expected')
這里我們用裝飾器將生成函數(shù)轉(zhuǎn)化為上下文管理器!
原文 Advanced Python Constructs
翻譯 reverland
|