《Python高級編程(第2版)》之語法最佳實踐


試讀: www.epubit.com.cn/book/detail…
購書: item.jd.com/12241204.ht…

編寫高效語法的能力會隨着時間逐步提升。回頭看看寫的第一個程序,你可能就會贊成這個觀點。正確的語法看起來賞心悅目,而錯誤的語法則使人煩惱。html


除了實現的算法與程序架構設計以外,還要特別注意的是,程序的寫法也會嚴重影響它將來的發展。許多程序被丟棄並從頭重寫,就是由於難懂的語法、不清晰的API或不合常理的標準。python


不過Python在最近幾年裏發生了很大變化。所以,若是你被鄰居(一個愛嫉妒的人,來自本地Ruby開發者用戶組)綁架了一段時間,而且遠離新聞,那麼你可能會對Python的新特性感到吃驚。從最先版本到目前的3.5版,這門語言已經作了許多改進,變得更加清晰、更加整潔、也更容易編寫。Python基礎知識並無發生很大變化,但如今使用的工具更符合人們的使用習慣。程序員


本章將介紹如今這門語言的語法中最重要的元素,以及它們的使用技巧,以下所示。算法



  • 列表推導(list comprehension)。

  • 迭代器(iterator)和生成器(generator)。

  • 描述符(descriptor)和屬性(property)。

  • 裝飾器(decorator)。

  • withcontextlib


速度提高或內存使用的代碼性能技巧將會在第十一、12章中講述。數據庫


2.1 Python的內置類型


Python提供了許多好用的數據類型,既包括數字類型,也包括集合類型。對於數字類型來講,語法並無什麼特別之處。固然,每種類型的定義會有些許差別,也有一些(可能)不太有名的運算符細節,但留給開發人員的選擇並很少。對於集合類型和字符串來講,狀況就發生變化了。雖然人們常說「作事的方法應該只有一種」,但留給Python開發人員的選擇確實有不少。在初學者看來,有些代碼模式看起來既直觀又簡單,但是有經驗的程序員每每會認爲它們不夠Pythonic,由於它們要麼效率低下,要麼就是過於囉嗦。編程


這種解決常見問題的Pythonic模式(許多程序員稱之爲習語[idiom])看起來每每只是美觀而已。但這種見解大錯特錯。大多數習語都揭示了Python的內部實現方式以及內置結構和模塊的工做原理。想要深刻理解這門語言,瞭解更多這樣的細節是很必要的。此外,社區自己也會受到關於Python工做原理的一些謠言和成見的影響。只有本身深刻鑽研,你纔可以分辨出關於Python的流行說法的真假。數組


2.1.1 字符串與字節


對於只用Python 2編程的程序員來講,字符串的話題可能會形成一些困惑。Python 3中只有一種可以保存文本信息的數據類型,就是str(string,字符串)。它是不可變的序列,保存的是Unicode碼位(code point)。這是與Python 2的主要區別,Python 2用str表示字節字符串,這種類型如今在Python 3中用bytes對象來處理(但處理方式並不徹底相同)。緩存


Python中的字符串是序列。基於這一事實,應該把字符串放在其餘容器類型的一節去介紹,但字符串與其餘容器類型在細節上有一個很重要的差別。字符串能夠保存的數據類型有很是明確的限制,就是Unicode文本。安全


bytes以及可變的bytearraystr不一樣,只能用字節做爲序列值,即0 <= x < 256範圍內的整數。一開始可能會有點糊塗,由於其打印結果與字符串很是類似:bash


>>> print(bytes([102, 111, 111]))
b'foo'複製代碼


對於bytesbytearray,在轉換爲另外一種序列類型(例如listtuple)時能夠顯示出其原本面目:


>>> list(b'foo bar')
[102, 111, 111, 32, 98, 97, 114]
>>> tuple(b'foo bar')
(102, 111, 111, 32, 98, 97, 114)複製代碼


許多關於Python 3的爭議都是關於打破字符串的向後兼容和Unicode的處理方式。從Python 3.0開始,全部沒有前綴的字符串都是Unicode。所以,全部用單引號(')、雙引號(")或成組的3個引號(單引號或雙引號)包圍且沒有前綴的值都表示str數據類型:


>>> type("some string")
< class 'str' >複製代碼


在Python 2中,Unicode須要有u前綴(例如u"some string")。從Python 3.3開始,爲保證向後兼容,仍然可使用這個前綴,但它在Python 3中沒有任何語法上的意義。


前面的一些例子中已經提到過字節,但爲了保持先後一致,咱們來明確介紹它的語法。字節也被單引號、雙引號或三引號包圍,但必須有一個bB前綴:


>>> type(b"some bytes")
< class 'bytes' >複製代碼


注意,Python語法中沒有bytearray字面值。


最後一樣重要的是,Unicode字符串中包含沒法用字節表示的「抽象」文本。所以,若是Unicode字符串沒有被編碼爲二進制數據的話,是沒法保存在磁盤中或經過網絡發送的。將字符串對象編碼爲字節序列的方法有兩種:



  • 利用str.encode(encoding, errors)方法,用註冊編解碼器(registered codec)對字符串進行編碼。編解碼器由encoding參數指定,默認值爲'utf-8'。第二個errors參數指定錯誤的處理方案,能夠取'strict'(默認值)、'ignore''replace''xmlcharrefreplace'或其餘任何註冊的處理程序(參見內置codecs模塊的文檔)。

  • 利用bytes(source, encoding, errors)構造函數,建立一個新的字節序列。若是sourcestr類型,那麼必須指定encoding參數,它沒有默認值。encodingerrors參數的用法與str.encode()方法中的相同。


用相似方法能夠將bytes表示的二進制數據轉換成字符串:



  • 利用bytes.decode(encoding, errors)方法,用註冊編解碼器對字節進行解碼。這一方法的參數含義及其默認值與str.encode()相同。

  • 利用str(source, encoding, error)構造函數,建立一個新的字符串實例。與bytes()構造函數相似,若是source是字節序列的話,必須指定str函數的encoding參數,它沒有默認值。



技巧.tif 


命名——字節與字節字符串的對比 


因爲Python 3中的變化,有些人傾向於將bytes實例稱爲字節字符串。這主要是因爲歷史緣由——Python 3中的bytes是與Python 2中的str類型最爲接近的序列類型(但並不徹底相同)。不過bytes實例是字節序列,也不須要表示文本數據。因此爲了不混淆,雖然bytes實例與字符串具備類似性,但建議始終將其稱爲bytes或字節序列。Python 3中字符串的概念是爲文本數據準備的,如今始終是str類型。



1.實現細節


Python字符串是不可變的。字節序列也是如此。這一事實很重要,由於它既有優勢又有缺點。它還會影響Python高效處理字符串的方式。因爲不變性,字符串能夠做爲字典的鍵或set的元素,由於一旦初始化以後字符串的值就不會改變。另外一方面,每當須要修改過的字符串時(即便只是微小的修改),都須要建立一個全新的字符串實例。幸運的是,bytearraybytes的可變版本,不存在這樣的問題。字節數組能夠經過元素賦值來進行原處修改(無需建立新對象),其大小也能夠像列表同樣動態地變化(利用appendpopinseer等方法)。


2.字符串拼接


因爲Python字符串是不可變的,在須要合併多個字符串實例時可能會產生一些問題。如前所述,拼接任意不可變序列都會生成一個新的序列對象。思考下面這個例子,利用多個字符串的重複拼接操做來建立一個新字符串:


s = ""
for substring in substrings:
s += substring複製代碼

這會致使運行時間成本與字符串總長度成二次函數關係。換句話說,這種方法效率極低。處理這種問題能夠用str.join()方法。它接受可迭代的字符串做爲參數,返回合併後的字符串。因爲這是一個方法,實際的作法是利用空字符串來調用它:


s = "".join(substrings)複製代碼

字符串的這一方法還能夠用於在須要合併的多個子字符串之間插入分隔符,看下面這個例子:


>>> ','.join(['some', 'comma', 'separated', 'values'])
'some,comma,separated,values'複製代碼


須要記住,僅僅由於join()方法速度更快(對於大型列表來講更是如此),並不意味着在全部須要拼接兩個字符串的狀況下都應該使用這一方法。雖然這是一種廣爲承認的作法,但並不會提升代碼的可讀性。可讀性是很重要的!在某些狀況下,join()的性能可能還不如利用加法的普通拼接,下面舉幾個例子。



  • 若是子字符串的數量不多,並且已經包含在某個可迭代對象中,那麼在某些狀況下,建立一個新序列來進行拼接操做的開銷可能會超過使用join()節省下來的開銷。

  • 在拼接短的字面值時,因爲CPython中的常數摺疊(constant folding),一些複雜的字面值(不僅是字符串)在編譯時會被轉換爲更短的形式,例如'a' + 'b' + 'c'被轉換爲'abc'。固然,這隻適用於相對短的常量(字面值)。


最後,若是事先知道字符串的數目,能夠用正確的字符串格式化方法來保證字符串拼接的最佳可讀性。字符串格式化能夠用str.format()方法或%運算符。若是代碼段的性能不是很重要,或者優化字符串拼接節省的開銷很小,那麼推薦使用字符串格式化做爲最佳方法。



技巧.tif 


常數摺疊和窺孔優化程序 


CPython對編譯過的源代碼使用窺孔優化程序來提升其性能。這種優化程序直接對Python字節碼實現了許多常見的優化。如上所述,常數摺疊就是其功能之一。生成常數的長度不得超過一個固定值。在Python 3.5中這個固定值仍然是 20。無論怎樣,這個具體細節只是爲了知足讀者的好奇心而已,並不能在平常編程中使用。窺孔優化程序還實現了許多有趣的優化,詳細信息請參見Python源代碼中的Python/peephole.c文件。



2.1.2 集合類型


Python提供了許多內置的數據集合類型,若是選擇明智的話,能夠高效解決許多問題。你可能已經學過下面這些集合類型,它們都有專門的字面值,以下所示。



  • 列表(list)。

  • 元組(tuple)。

  • 字典(dictionary)。

  • 集合(set)


Python的集合類型固然不止這4種,它的標準庫擴展了其可選列表。在許多狀況下,問題的答案可能正如選擇正確的數據結構同樣簡單。本書的這一部分將深刻介紹各類集合類型,以幫你作出更好的選擇。


1.列表與元組


Python最基本的兩個集合類型就是列表與元組,它們都表示對象序列。只要是花幾小時學過Python的人,應該都很容易發現兩者之間的根本區別:列表是動態的,其大小能夠改變;而元組是不可變的,一旦建立就不能修改。


雖然快速分配/釋放小型對象的優化方法有不少,但對於元素位置自己也是信息的數據結構來講,推薦使用元組這一數據類型。舉個例子,想要保存(x, y)座標對,元組多是一個很好的選擇。反正關於元組的細節至關無趣。本章關於元組惟一重要的內容就是,tuple不可變的(immutable),所以也是可哈希的(hashable)。其具體含義將會在後面「字典」一節介紹。比元組更有趣的是另外一種動態的數據結構list,以及它的工做原理和高效處理理方式。


(1)實現細節

許多程序員容易將Python的list類型與其餘語言(如C、C++或Java)標準庫中常見的鏈表的概念相混淆。事實上,CPython的列表根本不是列表。在CPython中,列表被實現爲長度可變的數組。對於其餘Python實現(如Jython和IronPython)而言,這種說法應該也是正確的,雖然這些項目的文檔中沒有記錄其實現細節。形成這種混淆的緣由很清楚。這種數據類型被命名爲列表,還和鏈表實現有類似的接口。


爲何這一點很重要,這又意味着什麼呢?列表是最多見的數據結構之一,其使用方式會對全部應用的性能帶來極大影響。此外,CPython又是最多見也最經常使用的Python實現,因此瞭解其內部實現細節相當重要。


從細節上來看,Python中的列表是由對其餘對象的引用組成的的連續數組。指向這個數組的指針及其長度被保存在一個列表頭結構中。這意味着,每次添加或刪除一個元素時,由引用組成的數組須要改變大小(從新分配)。幸運的是,Python在建立這些數組時採用了指數過度配(exponential over-allocation),因此並非每次操做都須要改變數組大小。這也是添加或取出元素的平攤複雜度較低的緣由。不幸的是,在普通鏈表中「代價很小」的其餘一些操做在Python中的計算複雜度卻相對較高:



  • 利用list.insert方法在任意位置插入一個元素——複雜度爲O(n)。

  • 利用list.deletedel刪除一個元素——複雜度爲O(n)。


這裏n是列表的長度。至少利用索引來查找或修改元素的時間開銷與列表大小無關。表2-1是一張完整的表格,列出了大多數列表操做的平均時間複雜度。


表2-1

操做

複雜度

複製

O(n)

添加元素

O(1)

插入元素

O(n)

獲取元素

O(1)

修改元素

O(1)

刪除元素

O(n)

遍歷

O(n)

獲取長度爲k的切片

O(k)

刪除切片

O(n)

修改長度爲k的切片

O(k+n)

列表擴展(Extend)

O(k)

乘以k

O(nk)

測試元素是否在列表中(element in list)

O(n)

min()/max()

O(n)

獲取列表長度

O(1)


對於須要真正的鏈表(或者簡單來講,雙端appendpop操做的複雜度都是O(1)的數據結構)的場景,Python在內置的collections模塊中提供了deque(雙端隊列)。它是棧和隊列的通常化,在須要用到雙向鏈表的地方均可以使用這種數據結構。


(2)列表推導

你可能知道,編寫這樣的代碼是很痛苦的:


>>> evens = []
>>> for i in range(10):
... if i % 2 == 0:
... evens.append(i)
...
>>> evens
[0, 2, 4, 6, 8]複製代碼


這種寫法可能適用於C語言,但在Python中的實際運行速度很慢,緣由以下。



  • 解釋器在每次循環中都須要判斷序列中的哪一部分須要修改。

  • 須要用一個計數器來跟蹤須要處理的元素。

  • 因爲append()是一個列表方法,因此每次遍歷時還須要額外執行一個查詢函數。


列表推導正是解決這個問題的正確方法。它使用編排好的功能對上述語法的一部分作了自動化處理:


>>> [i for i in range(10) if i % 2 == 0]
[0, 2, 4, 6, 8]複製代碼


這種寫法除了更加高效以外,也更加簡短,涉及的語法元素也更少。在大型程序中,這意味着更少的錯誤,代碼也更容易閱讀和理解。



技巧.tif 


列表推導和內部數組調整大小 


有些Python程序員中會謠傳這樣的說法:每添加幾個元素以後都要對錶示列表對象的內部數組大小進行調整,這個問題能夠用列表推導來解決。還有人說一次分配就能夠將數組大小調整到剛恰好。不幸的是,這些說法都是不正確的。


解釋器在對列表推導進行求值的過程當中並不知道最終結果容器的大小,也就沒法爲它預先分配數組的最終大小。所以,內部數組的從新分配方式與for循環中徹底相同。但在許多狀況下,與普通循環相比,使用列表推導建立列表要更加整潔、更加快速。



(3)其餘習語

Python習語的另外一個典型例子是使用enumerate(枚舉)。在循環中使用序列時,這個內置函數能夠很方便地獲取其索引。如下面這段代碼爲例:


>>> i = 0
>>> for element in ['one', 'two', 'three']:
... print(i, element)
... i += 1
...
0 one
1 two
2 three複製代碼


它能夠替換爲下面這段更短的代碼:


>>> for i, element in enumerate(['one', 'two', 'three']):
... print(i, element)
...
0 one
1 two
2 three複製代碼


若是須要一個一個合併多個列表(或任意可迭代對象)中的元素,那麼可使用內置的zip()函數。對兩個大小相等的可迭代對象進行均勻遍歷時,這是一種很是經常使用的模式:


>>> for item in zip([1, 2, 3], [4, 5, 6]):
... print(item)
...
(1, 4)
(2, 5)
(3, 6)複製代碼


注意,對zip()函數返回的結果再次調用zip(),能夠將其恢復原狀:


>>> for item in zip(zip([1, 2, 3], [4, 5, 6])):
... print(item)
...
(1, 2, 3)
(4, 5, 6)
複製代碼


另外一個經常使用的語法元素是序列解包(sequence unpacking)。這種方法並不限於列表和元組,而是適用於任意序列類型(甚至包括字符串和字節序列)。只要賦值運算符左邊的變量數目與序列中的元素數目相等,你均可以用這種方法將元素序列解包到另外一組變量中:


>>> first, second, third = "foo", "bar", 100
>>> first
'foo'
>>> second
'bar'
>>> third
100複製代碼


解包還能夠利用帶星號的表達式獲取單個變量中的多個元素,只要它的解釋沒有歧義便可。還能夠對嵌套序列進行解包。特別是在遍歷由序列構成的複雜數據結構時,這種方法很是實用。下面是一些更復雜的解包示例:


>>> # 帶星號的表達式能夠獲取序列的剩餘部分
>>> first, second, 複製代碼rest = 0, 1, 2, 3
>>> first
0
>>> second
1
>>> rest
[2, 3]

>>> # 帶星號的表達式能夠獲取序列的中間部分
>>> first, inner, last = 0, 1, 2, 3
>>> first
0
>>> inner
[1, 2]
>>> last
3

>>> # 嵌套解包
>>> (a, b), (c, d) = (1, 2), (3, 4)
>>> a, b, c, d
(1, 2, 3, 4)
複製代碼


2.字典


字典是Python中最通用的數據結構之一。dict能夠將一組惟一鍵映射到對應的值,以下所示:


{
1: ' one',
2: ' two',
3: ' three',
}複製代碼

字典是你應該已經瞭解的基本內容。無論怎樣,程序員還能夠用和前面列表推導相似的推導來建立一個新的字典。這裏有一個很是簡單的例子以下所示:


squares = {number: number**2 for number in range(100)}複製代碼

重要的是,使用字典推導具備與列表推導相同的優勢。所以在許多狀況下,字典推導要更加高效、更加簡短、更加整潔。對於更復雜的代碼而言,須要用到許多if語句或函數調用來建立一個字典,這時最好使用簡單的for循環,尤爲是它還提升了可讀性。


對於剛剛接觸Python 3的Python程序員來講,在遍歷字典元素時有一點須要特別注意。字典的keys()values()items()3個方法的返回值類型再也不是列表。此外,與之對應的iterkeys()itervalues()iteritems()原本返回的是迭代器,而Python 3中並無這3個方法。如今keys()values()items()返回的是視圖對象(view objects)。



  • keys():返回dict keys對象,能夠查看字典的全部鍵。

  • values():返回dict values對象,能夠查看字典的全部值。

  • it ems():返回dict _ items對象,能夠查看字典全部的(key, value)二元元組。


視圖對象能夠動態查看字典的內容,所以每次字典發生變化時,視圖都會相應改變,見下面這個例子:


>>> words = {'foo': 'bar', 'fizz': 'bazz'}
>>> items = words.items()
>>> words['spam'] = 'eggs'
>>> items
dictitems([('spam', 'eggs'), ('fizz', 'bazz'), ('foo', 'bar')])複製代碼


視圖對象既有舊的keys()values()items()方法返回的列表的特性,也有舊的iterkeys()itervalues()iteritems()方法返回的迭代器的特性。視圖無需冗餘地將全部值都保存在內存裏(像列表那樣),但你仍然能夠獲取其長度(使用len),也能夠測試元素是否包含其中(使用in子句)。固然,視圖是可迭代的。


最後一件重要的事情是,在keys()values()方法返回的視圖中,鍵和值的順序是徹底對應的。在Python 2中,若是你想保證獲取的鍵和值順序一致,那麼在兩次函數調用之間不能修改字典的內容。如今dict keysdict _ values是動態的,因此即便在調用keys()values()之間字典內容發生了變化,那麼這兩個視圖的元素遍歷順序也是徹底一致的。


(1)實現細節

CPython使用僞隨機探測(pseudo-random probing)的散列表(hash table)做爲字典的底層數據結構。這彷佛是很是高深的實現細節,但在短時間內不太可能發生變化,因此程序員也能夠把它當作一個有趣的事實來了解。


因爲這一實現細節,只有可哈希的(hashable)對象才能做爲字典的鍵。若是一個對象有一個在整個生命週期都不變的散列值(hash value),並且這個值能夠與其餘對象進行比較,那麼這個對象就是可哈希的。Python全部不可變的內置類型都是可哈希的。可變類型(如列表、字典和集合)是不可哈希的,所以不能做爲字典的鍵。定義可哈希類型的協議包括下面這兩個方法。



  • hash :這一方法給出dict內部實現須要的散列值(整數)。對於用戶自定義類的實例對象,這個值由id()給出。

  • eq :比較兩個對象的值是否相等。對於用戶自定義類,除了自身以外,全部實例對象默認不相等。


若是兩個對象相等,那麼它們的散列值必定相等。反之則不必定成立。這說明可能會發生散列衝突(hash collision),即散列值相等的兩個對象可能並不相等。這是容許的,全部Python實現都必須解決散列衝突。CPython用開放定址法(open addressing)來解決這一衝突(en.wikipedia.org/wiki/Open_a…


字典的3個基本操做(添加元素、獲取元素和刪除元素)的平均時間複雜度爲O(1),但它們的平攤最壞狀況複雜度要高得多,爲O(n),這裏的n是當前字典的元素數目。此外,若是字典的鍵是用戶自定義類的對象,而且散列方法不正確的話(發生衝突的風險很大),那麼這會給字典性能帶來巨大的負面影響。CPython字典的時間複雜度的完整表格如表2-2所示。


表2-2

操做

平均複雜度

平攤最壞狀況複雜度

獲取元素

O(1)

O(n)

修改元素

O(1)

O(n)

刪除元素

O(1)

O(n)

複製

O(n)

O(n)

遍歷

O(n)

O(n)


還有很重要的一點須要注意,在複製和遍歷字典的操做中,最壞狀況複雜度中的n是字典曾經達到的最大元素數目,而不是當前元素數目。換句話說,若是一個字典曾經元素個數不少,後來又大大減小了,那麼遍歷這個字典可能要花費至關長的時間。所以在某些狀況下,若是須要頻繁遍歷某個字典,那麼最好建立一個新的字典對象,而不是僅在舊字典中刪除元素。


(2)缺點和替代方案

使用字典的常見陷阱之一,就是它並不會按照鍵的添加順序來保存元素的順序。在某些狀況下,字典的鍵是連續的,對應的散列值也是連續值(例如整數),那麼因爲字典的內部實現,元素的順序可能和添加順序相同:


>>> {number: None for number in range(5)}.keys()
dict_keys([0, 1, 2, 3, 4])複製代碼


不過,若是使用散列方法不一樣的其餘數據類型,那麼字典就不會保存元素順序。下面是CPython中的例子:


>>> {str(number): None for number in range(5)}.keys()
dict_keys(['1', '2', '4', '0', '3'])
>>> {str(number): None for number in reversed(range(5))}.keys()
dict_keys(['2', '3', '1', '4', '0'])複製代碼


如上述代碼所示,字典元素的順序既與對象的散列方法無關,也與元素的添加順序無關。但咱們也不能徹底信賴這一說法,由於在不一樣的Python實現中可能會有所不一樣。


但在某些狀況下,開發者可能須要使用可以保存添加順序的字典。幸運的是,Python標準庫的collections模塊提供了名爲OrderedDict的有序字典。它選擇性地接受一個可迭代對象做爲初始化參數:


>>> from collections import OrderedDict
>>> OrderedDict((str(number), None) for number in range(5)).keys()
odictkeys(['0', '1', '2', '3', '4'])複製代碼


OrderedDict還有一些其餘功能,例如利用popitem()方法在雙端取出元素或者利用move to _ end()方法將指定元素移動到某一端。這種集合類型的完整參考可參見Python文檔(docs.python.org/3/library/c…


還有很重要的一點是,在很是老的代碼庫中,可能會用dict來實現原始的集合,以確保元素的惟一性。雖然這種方法能夠給出正確的結果,但只有在低於2.3的Python版本中才予以考慮。字典的這種用法十分浪費資源。Python有內置的set類型專門用於這個目的。事實上,CPython中set的內部實現與字典很是相似,但還提供了一些其餘功能,以及與集合相關的特定優化。


3.集合


集合是一種魯棒性很好的數據結構,當元素順序的重要性不如元素的惟一性和測試元素是否包含在集合中的效率時,大部分狀況下這種數據結構是頗有用的。它與數學上的集合概念很是相似。Python的內置集合類型有兩種。



  • set():一種可變的、無序的、有限的集合,其元素是惟一的、不可變的(可哈希的)對象。

  • frozenset():一種不可變的、可哈希的、無序的集合,其元素是惟一的、不可變的(可哈希的)對象。


因爲frozenset()具備不變性,它能夠用做字典的鍵,也能夠做爲其餘set()frozenset()的元素。在一個set()frozenset()中不能包含另外一個普通的可變set(),由於這會引起TypeError


>>> set([set([1,2,3]), set([2,3,4])])
Traceback (most recent call last):
File "< stdin >", line 1, in < module >
TypeError: unhashable type: 'set'複製代碼


下面這種集合初始化的方法是徹底正確的:


>>> set([frozenset([1,2,3]), frozenset([2,3,4])])
{frozenset({1, 2, 3}), frozenset({2, 3, 4})}
>>> frozenset([frozenset([1,2,3]), frozenset([2,3,4])])
frozenset({frozenset({1, 2, 3}), frozenset({2, 3, 4})})複製代碼


建立可變集合方法有如下3種,以下所示。



  • 調用set(),選擇性地接受可迭代對象做爲初始化參數,例如set([0, 1, 2])

  • 使用集合推導,例如{element for element in range(3)}

  • 使用集合字面值,例如{1, 2, 3}


注意,使用集合的字面值和推導要格外當心,由於它們在形式上與字典的字面值和推導很是類似。此外,空的集合對象是沒有字面值的。空的花括號{}表示的是空的字典字面值。


實現細節

CPython中的集合與字典很是類似。事實上,集合被實現爲帶有空值的字典,只有鍵纔是實際的集合元素。此外,集合還利用這種沒有值的映射作了其餘優化。


因爲這一點,能夠快速向集合添加元素、刪除元素或檢查元素是否存在,平均時間複雜度均爲O(1)。但因爲CPython的集合實現依賴於相似的散列表結構,所以這些操做的最壞狀況複雜度是O(n),其中n是集合的當前大小。


字典的其餘實現細節也適用於集合。集合中的元素必須是可哈希的,若是集合中用戶自定義類的實例的散列方法不佳,那麼將會對性能產生負面影響。


4.超越基礎集合類型——collections模塊


每種數據結構都有其缺點。沒有一種集合類型適合解決全部問題,4種基本類型(元組、列表、集合和字典)提供的選擇也不算多。它們是最基本也是最重要的集合類型,都有專門的語法。幸運的是,Python標準庫內置的collections模塊提供了更多的選擇。前面已經提到過其中一種(deque)。下面是這個模塊中最重要的集合類型。



  • namedtuple():用於建立元組子類的工廠函數(factory function),能夠經過屬性名來訪問它的元索引。

  • deque:雙端隊列,相似列表,是棧和隊列的通常化,能夠在兩端快速添加或取出元素。

  • ChainMap:相似字典的類,用於建立多個映射的單一視圖。

  • Counter:字典子類,因爲對可哈希對象進行計數。

  • OrderedDict:字典子類,能夠保存元素的添加順序。

  • defaultdict:字典子類,能夠經過調用用戶自定義的工廠函數來設置缺失值。



提示.tif  


第12章介紹了從collections模塊選擇集合類型的更多細節,也給出了關於什麼時候使用這些集合類型的建議。



2.2 高級語法


在一種語言中,很難客觀判斷哪些語法元素屬於高級語法。對於本章會講到的高級語法元素,咱們會講到這樣的元素,它們不與任何特定的內置類型直接相關,並且在剛開始學習時相對難以掌握。對於Python中難以理解的特性,其中最多見的是:



  • 迭代器(iterator)。

  • 生成器(generator)。

  • 裝飾器(decorator)。

  • 上下文管理器(context manager)。


2.2.1 迭代器


迭代器只不過是一個實現了迭代器協議的容器對象。它基於如下兩個方法。



  • next :返回容器的下一個元素。

  • iter :返回迭代器自己。


迭代器能夠利用內置的iter函數和一個序列來建立。看下面這個例子:


>>> i = iter('abc')
>>> next(i)
'a'
>>> next(i)
'b'
>>> next(i)
'c'
>>> next(i)
Traceback (most recent call last):
File "< input >", line 1, in < module >
StopIteration複製代碼


當遍歷完序列時,會引起一個StopIteration異常。這樣迭代器就能夠與循環兼容,由於能夠捕獲這個異常並中止循環。要建立自定義的迭代器,能夠編寫一個具備 next 方法的類,只要這個類提供返回迭代器實例的 iter 特殊方法:


class CountDown:
def init(self, step):
self.step = step
def next(self):
"""Return the next element."""
if self.step < = 0:
raise StopIteration
self.step -= 1
return self.step
def iter(self):
"""Return the iterator itself."""
return self複製代碼

下面是這個迭代器的用法示例:


>>> for element in CountDown(4):
... print(element)
...
3
2
1
0複製代碼


迭代器自己是一個底層的特性和概念,在程序中能夠不用它。但它爲生成器這一更有趣的特性提供了基礎。


2.2.2 yield語句


生成器提供了一種優雅的方法,可讓編寫返回元素序列的函數所需的代碼變得簡單、高效。基於yield語句,生成器能夠暫停函數並返回一箇中間結果。該函數會保存執行上下文,稍後在必要時能夠恢復。


舉個例子,斐波納契(Fibonacci)數列能夠用生成器語法來實現。下列代碼是來自於PEP 255(簡單生成器)文檔中的例子:


def fibonacci():
a, b = 0, 1
while True:
yield b
a, b = b, a + b複製代碼

你能夠用next()函數或for循環從生成器中獲取新的元素,就像迭代器同樣:


>>> fib = fibonacci()
>>> next(fib)
1
>>> next(fib)
1
>>> next(fib)
2
>>> [next(fib) for i in range(10)]
[3, 5, 8, 13, 21, 34, 55, 89, 144, 233]複製代碼


這個函數返回一個generator對象,是特殊的迭代器,它知道如何保存執行上下文。它能夠被無限次調用,每次都會生成序列的下一個元素。這種語法很簡潔,算法可無限調用的性質並無影響代碼的可讀性。沒必要提供使函數中止的方法。實際上,它看上去就像用僞代碼設計的數列同樣。


在社區中,生成器並不經常使用,由於開發人員還不習慣這種思考方式。多年來,開發人員已經習慣於使用直截了當的函數。每次你須要返回一個序列的函數或在循環中運行的函數時,都應該考慮使用生成器。當序列元素被傳遞到另外一個函數中以進行後續處理時,一次返回一個元素能夠提升總體性能。


在這種狀況下,用於處理一個元素的資源一般不如用於整個過程的資源重要。所以,它們能夠保持位於底層,使程序更加高效。舉個例子,斐波那契數列是無窮的,但用來生成它的生成器每次提供一個值,並不須要無限大的內存。一個常見的應用場景是使用生成器的數據流緩衝區。使用這些數據的第三方代碼能夠暫停、恢復和中止生成器,在開始這一過程以前無需導入全部數據。


舉個例子,來自標準庫的tokenize模塊能夠從文本流中生成令牌(token),並對處理過的每一行都返回一個迭代器,以供後續處理:


>>> import tokenize
>>> reader = open('hello.py').readline
>>> tokens = tokenize.generate_tokens(reader)
>>> next(tokens)
TokenInfo(type=57 (COMMENT), string='# -複製代碼- coding: utf-8 --', start=(1,
0), end=(1, 23), line='# -
- coding: utf-8 --\n')
>>> next(tokens)
TokenInfo(type=58 (NL), string='\n', start=(1, 23), end=(1, 24), line='#
-
- coding: utf-8 --\n')
>>> next(tokens)
TokenInfo(type=1 (NAME), string='def', start=(2, 0), end=(2, 3),
line='def helloworld():\n')
複製代碼


從這裏能夠看出,open遍歷文件的每一行,而generate tokens則利用管道對其進行遍歷,完成一些額外的工做。對於基於某些序列的數據轉換算法而言,生成器還有助於下降算法複雜度並提升效率。把每一個序列看做一個iterator,而後再將其合併爲一個高階函數,這種方法能夠有效避免函數變得龐大、醜陋、沒有可讀性。此外,這種方法還能夠爲整個處理鏈提供實時反饋。


在下面的示例中,每一個函數都定義了一個對序列的轉換。而後將這些函數連接起來並應用。每次調用都將處理一個元素並返回其結果:


def power(values):
for value in values:
print('powering %s' % value)
yield value
def adder(values):
for value in values:
print('adding to %s' % value)
if value % 2 == 0:
yield value + 3
else:
yield value + 2複製代碼

將這些生成器合併使用,可能的結果以下:


>>> elements = [1, 4, 7, 9, 12, 19]
>>> results = adder(power(elements))
>>> next(results)
powering 1
adding to 1
3
>>> next(results)
powering 4
adding to 4
7
>>> next(results)
powering 7
adding to 7
9複製代碼



技巧.tif 


保持代碼簡單,而不是保持數據簡單  


最好編寫多個處理序列值的簡單可迭代函數,而不要編寫一個複雜函數,同時計算出整個集合的結果。



Python生成器的另外一個重要特性,就是可以利用next函數與調用的代碼進行交互。yield變成了一個表達式,而值能夠經過名爲send的新方法來傳遞:


def psychologist():
print('Please tell me your problems')
while True:
answer = (yield)
if answer is not None:
if answer.endswith('?'):
print("Don't ask yourself too much questions")
elif 'good' in answer:
print("Ahh that's good, go on")
elif 'bad' in answer:
print("Don't be so negative")複製代碼

下面是調用psychologist()函數的示例會話:


>>> free = psychologist()
>>> next(free)
Please tell me your problems
>>> free.send('I feel bad')
Don't be so negative
>>> free.send("Why I shouldn't ?")
Don't ask yourself too much questions
>>> free.send("ok then i should find what is good for me")
Ahh that's good, go on複製代碼


send的做用和next相似,但會將函數定義內部傳入的值變成yield的返回值。所以,這個函數能夠根據客戶端代碼來改變自身行爲。爲完成這一行爲,還添加了另外兩個函數:throwclose。它們將向生成器拋出錯誤。



  • throw:容許客戶端代碼發送要拋出的任何類型的異常。

  • close:做用相同,但會引起特定的異常——GeneratorExit。在這種狀況下,生成器函數必須再次引起GeneratorExitStopIteration



提示.tif  


生成器是Python中協程、異步併發等其餘概念的基礎,這些概念將在第13章介紹。



2.2.3 裝飾器


Python裝飾器的做用是使函數包裝與方法包裝(一個函數,接受函數並返回其加強函數)變得更容易閱讀和理解。最初的使用場景是在方法定義的開頭可以將其定義爲類方法或靜態方法。若是不用裝飾器語法的話,定義可能會很是稀疏,而且不斷重複:


class WithoutDecorators:
def some_static_method():
print("this is static method")
some_static_method = staticmethod(some_static_method)
def some_class_method(cls):
print("this is class method")
some_class_method = classmethod(some_class_method)複製代碼

若是用裝飾器語法重寫的話,代碼會更簡短,也更容易理解:


class WithDecorators:
@staticmethod
def some_static_method():
print("this is static method")

@classmethod
def some_class_method(cls):
print("this is class method")複製代碼

1.通常語法和可能的實現


裝飾器一般是一個命名的對象(不容許使用lambda表達式),在被(裝飾函數)調用時接受單一參數,並返回另外一個可調用對象。這裏用的是「可調用(callable)」。而不是以前覺得的「函數」。裝飾器一般在方法和函數的範圍內進行討論,但它的適用範圍並不侷限於此。事實上,任何可調用對象(任何實現了 call 方法的對象都是可調用的)均可以用做裝飾器,它們返回的對象每每也不是簡單的函數,而是實現了本身的 call 方法的更復雜的類的實例。


裝飾器語法只是語法糖而已。看下面這種裝飾器用法:


@some_decorator
def decorated_function():
pass複製代碼

這種寫法老是能夠替換爲顯式的裝飾器調用和函數的從新賦值:


def decorated_function():
pass
decorated_function = some_decorator(decorated_function)複製代碼

可是,若是在一個函數上使用多個裝飾器的話,後一種寫法的可讀性更差,也很是難以理解。



技巧.tif 


裝飾器甚至不須要返回可調用對象! 


事實上,任何函數均可以用做裝飾器,由於Python並無規定裝飾器的返回類型。所以,將接受單一參數但不返回可調用對象的函數(例如str)用做裝飾器,在語法上是徹底有效的。若是用戶嘗試調用這樣裝飾過的對象,最後終究會報錯。無論怎樣,針對這種裝飾器語法能夠作一些有趣的試驗。



(1)做爲一個函數

編寫自定義裝飾器有許多方法,但最簡單的方法就是編寫一個函數,返回包裝原始函數調用的一個子函數。


通用模式以下:


def mydecorator(function):
def wrapped(複製代碼args, kwargs):
# 在調用原始函數以前,作點什麼
result = function(*args,
kwargs)
# 在函數調用以後,作點什麼,
# 並返回結果
return result
# 返回wrapper做爲裝飾函數
return wrapped複製代碼

(2)做爲一個類

雖然裝飾器幾乎老是能夠用函數實現,但在某些狀況下,使用用戶自定義類可能更好。若是裝飾器須要複雜的參數化或者依賴於特定狀態,那麼這種說法每每是對的。


非參數化裝飾器用做類的通用模式以下:


class DecoratorAsClass:
def init(self, function):
self.function = function

def call(self, args, **kwargs):
# 在調用原始函數以前,作點什麼
result = self.function(
args, kwargs)
# 在調用函數以後,作點什麼,
# 並返回結果
return result
複製代碼

(3)參數化裝飾器

在實際代碼中一般須要使用參數化的裝飾器。若是用函數做爲裝飾器的話,那麼解決方法很簡單:須要用到第二層包裝。下面一個簡單的裝飾器示例,給定重複次數,每次被調用時都會重複執行一個裝飾函數:


def repeat(number=3):
"""屢次重複執行裝飾函數。

返回最後一次原始函數調用的值做爲結果
:param number: 重複次數,默認值是3
"""
def actual_decorator(function):
def wrapper(*args, 複製代碼kwargs):
result = None
for _ in range(number):
result = function(args, **kwargs)
return result
return wrapper
return actual_decorator
複製代碼

這樣定義的裝飾器能夠接受參數:


>>> @repeat(2)
... def foo():
... print("foo")
...
>>> foo()
foo
foo複製代碼


注意,即便參數化裝飾器的參數有默認值,但名字後面也必須加括號。帶默認參數的裝飾器的正確用法以下:


>>> @repeat()
... def bar():
... print("bar")
...
>>> bar()
bar
bar
bar複製代碼


沒加括號的話,在調用裝飾函數時會出現如下錯誤:


>>> @repeat
... def bar():
... pass
...
>>> bar()
Traceback (most recent call last):
File "< input >", line 1, in < module >
TypeError: actual_decorator() missing 1 required positional
argument: 'function'複製代碼


(4)保存內省的裝飾器

使用裝飾器的常見錯誤是在使用裝飾器時不保存函數元數據(主要是文檔字符串和原始函數名)。前面全部示例都存在這個問題。裝飾器組合建立了一個新函數,並返回一個新對象,但卻徹底沒有考慮原始函數的標識。這將會使得調試這樣裝飾過的函數更加困難,也會破壞可能用到的大多數自動生成文檔的工具,由於沒法訪問原始的文檔字符串和函數簽名。


但咱們來看一下細節。假設咱們有一個虛設的(dummy)裝飾器,僅有裝飾做用,還有其餘一些被裝飾的函數:


def dummy_decorator(function):
def wrapped(複製代碼args, kwargs):
"""包裝函數內部文檔。"""
return function(*args,
kwargs)
return wrapped

@dummy_decorator
def function_with_importantdocstring():
"""這是咱們想要保存的重要文檔字符串。"""
複製代碼

若是咱們在Python交互式會話中查看function with important docstring(),會注意到它已經失去了原始名稱和文檔字符串:


>>> function_with_important_docstring.name
'wrapped'
>>> function_with_important_docstring.doc
'包裝函數內部文檔。'複製代碼


解決這個問題的正確方法,就是使用functools模塊內置的wraps()裝飾器:


from functools import wraps

def preserving_decorator(function):
@wraps(function)
def wrapped(args, **kwargs):
"""包裝函數內部文檔。"""
return function(
args, kwargs)
return wrapped

@preserving_decorator
def function_with_important_docstring():
"""這是咱們想要保存的重要文檔字符串。"""
複製代碼

這樣定義的裝飾器能夠保存重要的函數元數據:


>>> function_with_important_docstring.name
'function_with_important_docstring.'
>>> function_with_important_docstring.doc
'這是咱們想要保存的重要文檔字符串。'複製代碼


2.用法和有用的例子


因爲裝飾器在模塊被首次讀取時由解釋器來加載,因此它們的使用應受限於通用的包裝器(wrapper)。若是裝飾器與方法的類或所加強的函數簽名綁定,那麼應該將其重構爲常規的可調用對象,以免複雜性。在任何狀況下,裝飾器在處理API時,一個好的作法是將它們彙集在一個易於維護的模塊中。


常見的裝飾器模式以下所示。



  • 參數檢查。

  • 緩存。

  • 代理。

  • 上下文提供者。


(1)參數檢查

檢查函數接受或返回的參數,在特定上下文中執行時可能有用。舉個例子,若是一個函數要經過XML-RPC來調用,那麼Python沒法像靜態語言那樣直接提供其完整簽名。當XML-RPC客戶端請求函數簽名時,就須要用這個功能來提供內省能力。



技巧.tif 


XML-RPC協議


XML-RPC協議是一種輕量級的遠程過程調用(Remote Procedure Call)協議,經過HTTP使用XML對調用進行編碼。對於簡單的客戶端-服務器交換,一般使用這種協議而不是SOAP。SOAP提供了列出全部可調用函數的頁面(WSDL),XML-RPC與之不一樣,並無可用函數的目錄。該協議提出了一個擴展,能夠用來發現服務器API,Python的xmlrpc模塊實現了這一擴展(參見docs.python.org/3/library/x…



自定義裝飾器能夠提供這種類型的簽名,並確保輸入和輸出表明自定義的簽名參數:


rpcinfo = {}

def xmlrpc(in
=(), out=(type(None),)):
def _xmlrpc(function):
# 註冊簽名
func_name = function.name
rpc_info[funcname] = (in, out)
def _check_types(elements, types):
"""用來檢查類型的子函數。"""
if len(elements) != len(types):
raise TypeError('argument count is wrong')
typed = enumerate(zip(elements, types))
for index, couple in typed:
arg, of_the_right_type = couple
if isinstance(arg, of_the_right_type):
continue
raise TypeError(
'arg #%d should be %s' % (index,
of_the_right_type))

# 包裝過的函數
def xmlrpc(args): # 沒有容許的關鍵詞
# 檢查輸入的內容
checkable_args = args[1:] # 去掉self
_check_types(checkableargs, in)
# 運行函數
res = function(
args)
# 檢查輸出的內容
if not type(res) in (tuple, list):
checkable_res = (res,)
else:
checkable_res = res
_check_types(checkable_res, out)

# 函數及其類型檢查成功
return res
return
xmlrpc
return xmlrpc複製代碼

裝飾器將函數註冊到全局字典中,並將其參數和返回值保存在一個類型列表中。注意,這個示例作了很大的簡化,爲的是展現裝飾器的參數檢查功能。


使用示例以下:


class RPCView:
@xmlrpc((int, int)) # two int -> None
def meth1(self, int1, int2):
print('received %d and %d' % (int1, int2))

@xmlrpc((str,), (int,)) # string -> int
def meth2(self, phrase):
print('received %s' % phrase)
return 12複製代碼

在實際讀取時,這個類定義會填充rpc infos字典,並用於檢查參數類型的特定環境中:


>>> rpc_info
{'meth2': ((< class 'str'>,), (< class 'int'>,)), 'meth1': ((< class
'int'>, < class 'int'>), (,))}
>>> my = RPCView()
>>> my.meth1(1, 2)
received 1 and 2
>>> my.meth2(2)
Traceback (most recent call last):
File "< input>", line 1, in < module>
File "< input>", line 26, in xmlrpc
File "< input>", line 20, in _check_types
TypeError: arg #0 should be < class 'str'>
複製代碼


(2)緩存

緩存裝飾器與參數檢查十分類似,不過它重點是關注那些內部狀態不會影響輸出的函數。每組參數均可以連接到惟一的結果。這種編程風格是函數式編程(functional programming,參見en.wikipedia.org/wiki/Functi…


所以,緩存裝飾器能夠將輸出與計算它所須要的參數放在一塊兒,並在後續的調用中直接返回它。這種行爲被稱爲memoizing(參見en.wikipedia.org/wiki/Memoiz…


import time
import hashlib
import pickle

cache = {}

def is_obsolete(entry, duration):
return time.time() - entry['time'] > duration

def compute_key(function, args, kw):
key = pickle.dumps((function.複製代碼name, args, kw))
return hashlib.sha1(key).hexdigest()

def memoize(duration=10):
def _memoize(function):
def
memoize(*args, 複製代碼kw):
key = compute_key(function, args, kw)

# 是否已經擁有它了?
if (key in cache and
not is_obsolete(cache[key], duration)):
print('we got a winner')
return cache[key]['value']
# 計算
result = function(args, **kw)
# 保存結果
cache[key] = {
'value': result,
'time': time.time()
}
return result
return memoize
return _memoize
複製代碼

利用已排序的參數值來構建SHA哈希鍵,並將結果保存在一個全局字典中。利用pickle來創建hash,這是凍結全部做爲參數傳入的對象狀態的快捷方式,以確保全部參數都知足要求。舉個例子,若是用一個線程或套接字做爲參數,那麼會引起PicklingError(參見docs.python.org/3/library/p…duration參數的做用是,若是上一次函數調用已通過去了太長時間,那麼它會使緩存值無效。


下面是一個使用示例:


>>> @memoize()
... def very_very_very_complex_stuff(a, b):
... # 若是在執行這個計算時計算機過熱
... # 請考慮停止程序
... return a + b
...
>>> very_very_very_complex_stuff(2, 2)
4
>>> very_very_very_complex_stuff(2, 2)
we got a winner
4
>>> @memoize(1) # 1秒後令緩存失效
... def very_very_very_complex_stuff(a, b):
... return a + b
...
>>> very_very_very_complex_stuff(2, 2)
4
>>> very_very_very_complex_stuff(2, 2)
we got a winner
4
>>> cache
{'c2727f43c6e39b3694649ee0883234cf': {'value': 4, 'time':
1199734132.7102251)}
>>> time.sleep(2)
>>> very_very_very_complex_stuff(2, 2)
4複製代碼


緩存代價高昂的函數能夠顯著提升程序的整體性能,但必須當心使用。緩存值還能夠與函數自己綁定,以管理其做用域和生命週期,代替集中化的字典。但在任何狀況下,更高效的裝飾器會使用基於高級緩存算法的專用緩存庫。



提示.tif  


第12章將會介紹與緩存相關的詳細信息和技術。



(3)代理

代理裝飾器使用全局機制來標記和註冊函數。舉個例子,一個根據當前用戶來保護代碼訪問的安全層可使用集中式檢查器和相關的可調用對象要求的權限來實現:


class User(object):
def 複製代碼init(self, roles):
self.roles = roles

class Unauthorized(Exception):
pass

def protect(role):
def _protect(function):
def
protect(複製代碼args, kw):
user = globals().get('user')
if user is None or role not in user.roles:
raise Unauthorized("I won't tell you")
return function(*args,
kw)
return protect
return _protect
複製代碼

這一模型經常使用於Python Web框架中,用於定義可發佈類的安全性。例如,Django提供裝飾器來保護函數訪問的安全。


下面是一個示例,當前用戶被保存在一個全局變量中。在方法被訪問時裝飾器會檢查他/她的角色:


>>> tarek = User(('admin', 'user'))
>>> bill = User(('user',))
>>> class MySecrets(object):
... @protect('admin')
... def waffle_recipe(self):
... print('use tons of butter!')
...
>>> these_are = MySecrets()
>>> user = tarek
>>> these_are.waffle_recipe()
use tons of butter!
>>> user = bill
>>> these_are.waffle_recipe()
Traceback (most recent call last):
File "< stdin>", line 1, in < module>
File "< stdin>", line 7, in wrap
main.Unauthorized: I won't tell you複製代碼


(4)上下文提供者

上下文裝飾器確保函數能夠運行在正確的上下文中,或者在函數先後運行一些代碼。換句話說,它設定並復位一個特定的執行環境。舉個例子,當一個數據項須要在多個線程之間共享時,就要用一個鎖來保護它避免屢次訪問。這個鎖能夠在裝飾器中編寫,代碼以下:


from threading import RLock
lock = RLock()

def synchronized(function):
def _synchronized(args, **kw):
lock.acquire()
try:
return function(
args, **kw)
finally:
lock.release()
return _synchronized

@synchronized
def thread_safe(): # 確保鎖定資源
pass複製代碼

上下文裝飾器一般會被上下文管理器(with語句)替代,後者將在本章後面介紹。


2.2.4 上下文管理器——with語句


爲了確保即便在出現錯誤的狀況下也能運行某些清理代碼,try...finally語句是頗有用的。這一語句有許多使用場景,例如:



  • 關閉一個文件。

  • 釋放一個鎖。

  • 建立一個臨時的代碼補丁。

  • 在特殊環境中運行受保護的代碼。


with語句爲這些使用場景下的代碼塊包裝提供了一種簡單方法。即便該代碼塊引起了異常,你也能夠在其執行先後調用一些代碼。例如,處理文件一般採用這種方式:


>>> hosts = open('/etc/hosts')
>>> try:
... for line in hosts:
... if line.startswith('#'):
... continue
... print(line.strip())
... finally:
... hosts.close()
...
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost複製代碼



提示.tif  


本示例只針對Linux系統,由於要讀取位於etc文件夾中的主機文件,但任何文本文件均可以用相同的方法來處理。



利用with語句,上述代碼能夠重寫爲:


>>> with open('/etc/hosts') as hosts:
... for line in hosts:
... if line.startswith('#'):
... continue
... print(line.strip())
...
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost複製代碼


在前面的示例中,open的做用是上下文管理器,確保即便出現異常也要在執行完for循環以後關閉文件。


與這條語句兼容的其餘項目是來自threading模塊的類:



  • threading.Lock

  • threading.RLock

  • threading.Condition

  • threading.Semaphore

  • threading.BoundedSemaphore


通常語法和可能的實現


with語句的通常語法的最簡單形式以下:


with context_manager:
# 代碼塊
...複製代碼

此外,若是上下文管理器提供了上下文變量,能夠用as子句保存爲局部變量:


with context_manager as context:
# 代碼塊
...複製代碼

注意,多個上下文管理器能夠同時使用,以下所示:


with A() as a, B() as b:
...複製代碼

這種寫法等價於嵌套使用,以下所示:


with A() as a:
with B() as b:
...複製代碼

(1)做爲一個類

任何實現了上下文管理器協議(context manager protocol)的對象均可以用做上下文管理器。該協議包含兩個特殊方法。



簡而言之,執行with語句的過程以下:



  • 調用 enter 方法。任何返回值都會綁定到指定的as子句。

  • 執行內部代碼塊。

  • 調用 exit 方法。


exit 接受代碼塊中出現錯誤時填入的3個參數。若是沒有出現錯誤,那麼這3個參數都被設爲None。出現錯誤時, exit 不該該從新引起這個錯誤,由於這是調用者(caller)的責任。但它能夠經過返回True來避免引起異常。這可用於實現一些特殊的使用場景,例以下一節將會看到的contextmanager裝飾器。但在大多數使用場景中,這一方法的正確行爲是執行相似於finally子句的一些清理工做,不管代碼塊中發生了什麼,它都不會返回任何內容。


下面是某個實現了這一協議的上下文管理器示例,以更好地說明其工做原理:


class ContextIllustration:
def 複製代碼enter(self):
print('entering context')
def
exit(self, exc_type, exc_value, traceback):
print('leaving context')

if exc_type is None:
print('with no error')
else:
print('with an error (%s)' % exc_value)
複製代碼

沒有引起異常時的運行結果以下:


>>> with ContextIllustration():
... print("inside")
...
entering context
inside
leaving context
with no error複製代碼


引起異常時的輸出以下:


>>> with ContextIllustration():
... raise RuntimeError("raised within 'with'")
...
entering context
leaving context
with an error (raised within 'with')
Traceback (most recent call last):
File "< input >", line 2, in < module >
RuntimeError: raised within 'with'複製代碼


(2)做爲一個函數——contextlib模塊

使用相似乎是實現Python語言提供的任何協議最靈活的方法,但對許多使用場景來講可能樣板太多。標準庫中新增了contextlib模塊,提供了與上下文管理器一塊兒使用的輔助函數。它最有用的部分是contextmanager裝飾器。你能夠在一個函數裏面同時提供 enter exit 兩部分,中間用yield語句分開(注意,這樣函數就變成了生成器)。用這個裝飾器編寫前面的例子,其代碼以下:


from contextlib import contextmanager

@contextmanager
def contextillustration():
print('entering context')

try:
yield
except Exception as e:
print('leaving context')
print('with an error (%s)' % e)
# 須要再次拋出異常
raise
else:
print('leaving context')
print('with no error')
複製代碼

若是出現任何異常,該函數都須要再次拋出這個異常,以便傳遞它。注意,context illustration在須要時能夠有一些參數,只要在調用時提供這些參數便可。這個小的輔助函數簡化了常規的基於類的上下文API,正如生成器對基於類的迭代器API的做用同樣。


這個模塊還提供了其餘3個輔助函數。



  • closing(element):返回一個上下文管理器,在退出時會調用該元素的close方法。例如,它對處理流的類就頗有用。

  • supress(*exceptions):它會壓制發生在with語句正文中的特定異常。

  • redirect stdout(new target)redirect stderr(new target):它會將代碼塊內任何代碼的sys.stdoutsys.stderr輸出重定向到類文件(file-like)對象的另外一個文件。


2.3 你可能還不知道的其餘語法元素


Python語法中有一些元素不太常見,也不多用到。這是由於它們能提供的好處不多,或者它們的用法很難記住。所以,許多Python程序員(即便有多年的經驗)徹底不知道這些語法元素的存在。其中最有名的例子以下:



  • for ... else語句。

  • 函數註解(function annotation)。


2.3.1 for ... else ...語句


for循環以後使用else子句,能夠在循環「天然」結束而不是被break語句終止時執行一個代碼塊:


>>> for number in range(1):
... break
... else:
... print("no break")
...
>>>
>>> for number in range(1):
... pass
... else:
... print("break")
...
break複製代碼


這一語句在某些狀況下頗有用,由於它有助於刪除一些「哨兵(sentinel)」變量,若是出現break時用戶想要保存信息,可能會須要這些變量。這使得代碼更加清晰,但可能會使不熟悉這種語法的程序員感到困惑。有人說else子句的這種含義是違反直覺的,但這裏介紹一個簡單的技巧,能夠幫你記住它的用法:for循環以後else子句的含義是「沒有break」。


2.3.2 函數註解


函數註解是Python 3最獨特的功能之一。官方文檔是這麼說的:函數註解是關於用戶自定義函數使用的類型的徹底可選的元信息,但事實上,它並不侷限於類型提示,並且在Python及其標準庫中也沒有單個功能能夠利用這種註解。這就是這個功能獨特的緣由:它沒有任何語法上的意義。能夠爲函數定義註解,並在運行時獲取這些註解,但僅此而已。如何使用註解留給開發人員去思考。


1.通常語法


對Python官方文檔中的示例稍做修改,就能夠很好展現如何定義並獲取函數註解:


>>> def f(ham: str, eggs: str = 'eggs') -> str:
... pass
...
>>> print(f.annotations)
{'return': < class 'str' >, 'eggs': < class 'str' >, 'ham': < class 'str' >}複製代碼


如上所述,參數註解的定義爲冒號後計算註解值的表達式。返回值註解的定義爲表示def語句結尾的冒號與參數列表以後的-&gt;之間的表達式。


定義好以後,註解能夠經過函數對象的 annotations __屬性獲取,它是一個字典,在應用運行期間能夠獲取。


任何表達式均可以用做註解,其位置靠近默認參數,這樣能夠建立一些迷惑人的函數定義,以下所示:


>>> def square(number: 0< =3 and 1=0) - > (\
... +9000): return number**2
>>> square(10)
100複製代碼


不過,註解的這種用法只會讓人糊塗,沒有任何其餘做用。即便不用註解,編寫出難以閱讀和理解的代碼也是相對容易的。


2.可能的用法


雖然註解有很大的潛力,但並無被普遍使用。一篇介紹Python 3新增功能的文章(參見docs.python.org/3/whatsnew/…PEP 3107列出如下可能的使用場景:



  • 提供類型信息。

    • 類型檢查。

    • 讓IDE顯示函數接受和返回的類型。

    • 函數重載/通用函數。

    • 與其餘語言之間的橋樑。

    • 適配。

    • 謂詞邏輯函數。

    • 數據庫查詢映射。

    • RPC參數編組。



  • 其餘信息。

    • 參數和返回值的文檔。




雖然函數註解存在的時間和Python 3同樣長,但仍然很難找到任一常見且積極維護的包,將函數註解用做類型檢查以外的功能。因此函數註解仍主要用於試驗和玩耍,這也是Python 3最初發布時包含該功能的最初目的。


2.4 小結


本章介紹了不直接與Python類和麪向對象編程相關的多個最佳語法實踐。本章第一部分重點介紹了與Python序列和集合相關的語法特性,也討論了字符串和字節相關的序列。本章其他部分介紹了兩組獨立的語法元素:一組是初學者相對難以理解的(例如迭代器、生成器和裝飾器),另外一組是不爲人知的(for...else子句和函數註解)。

</div>複製代碼
相關文章
相關標籤/搜索