[TOC]
我們已經討論了很多Python的內置類型和習慣用法,乍一看,它們似乎更像是在非面向對象的偽裝下訪問對象。在本章中,我們將討論看起來結構化的`for`循環是如何構成一套面向對象原則的輕量級包裝。我們也會看到該語法的各種擴展,它們將自動創建更多對象類型。我們將學習:
* 什么是設計模式
* 迭代器協議——最強大的設計模式之一
* 列表解析、集合解析和字典解析
* 生成器和協程
## 設計模式概要
當工程師和建筑師決定建造一座橋、一座塔或一座建筑時,他們遵循某些原則來確保結構的完整性。有各種各樣的可能的橋梁設計(例如懸索橋或懸臂橋),但是如果工程師沒有使用某種標準設計,將不會有一個更好的設計,他/她設計的橋可能會倒塌。
</b>
設計模式是一種嘗試,旨在為軟件工程引入正確設計結構的相同的形式化定義。有許多不同的設計模式用來解決不同的一般性問題。創建設計模式的人首先識別開發人員在各種情況下面臨的通用問題。然后他們從面向對象設計角度提出解決這個問題的理想方案。
</b>
然而,知道一個設計模式并選擇在我們的軟件中使用它,并不能
確保我們正在創建一個“正確”的解決方案。1907年,魁北克大橋(至今,它仍是世界上最長的懸臂橋)在施工完成前倒塌,因為設計工程師嚴重低估了用來建造它的鋼的重量。同樣,在軟件開發中,我們可能也會錯誤地選擇或應用設計模式,在正常運行情況下或當壓力超過其原始設計極限時,造成軟件“崩潰”。
</b>
任何一種設計模式都提出一組對象,如何以特定方式交互,用以解決一個普遍的問題。程序員的工作是識別他們所面對的特定問題,并在解決方案中調整總體設計。
</b>
在本章中,我們將討論迭代器設計模式。這種模式是如此強大且普遍,為Python開發人員提供了多種語法來訪問模式背后的面向對象原則。我們將在下兩章涵蓋其他設計模式。其中一些有語言支持,另外一些沒有語言支持,但是它們中沒有一個像迭代器模式那樣成為Python程序員的日常生活中固有的一部分。
## 迭代器
在典型的設計模式中,迭代器是一個具有`next()`方法和`done()`方法的對象;如果序列中沒有剩余的項目,后者返回`True`。在沒有內置迭代器支持的編程語言,迭代器將像這樣循環:
```
while not iterator.done():
item = iterator.next()
# 用item做些事情
```
在Python中,迭代是一種特殊的屬性,因此這個方法有一個特殊的名稱,`__next__`。可以使用`next(iterator)`內置函數來訪問該方法。而`done`方法不太一樣,迭代器協議會引發`StopIteration`來通知循環它已經完成。最后,我們有可讀性更強的`for item in iterator`的迭代器語法,用于實際訪問迭代器中的項,而不是更麻煩的`while`循環。讓我們看看更多細節。
### 迭代器協議
`collections. abc`模塊中的抽象基類`Iterator`定義了Python中的迭代器協議。如上所述,它必須有一個`__next__`方法,用于`for`循環(和其他支持迭代的特性)調用該方法,從
序列中得到一個新的元素。此外,每個迭代器還必須包含`Iterable`接口。任何提供`__iter__`方法的類是可迭代的(譯注:如果沒有`__iter__`方法,是沒辦法生成一個迭代器的),該方法必須返回一個`Iterator`實例,實例將覆蓋該類中的所有元素。因為迭代器已經循環遍歷元素,它的`__iter__`函數(譯注:就是`__iter__`方法)傳統上返回自己。
</b>
這聽起來可能有點混亂,所以看看下面的例子,但是要注意
這是解決這個問題的非常冗長的方法。它清楚地解釋了迭代和這兩個協議之間存在的問題,在本章后面我們將研究獲得這種效果的幾種更易讀的方法來:
```
class CapitalIterable:
def __init__(self, string):
self.string = string
def __iter__(self):
return CapitalIterator(self.string)
class CapitalIterator:
def __init__(self, string):
self.words = [w.capitalize() for w in string.split()]
self.index = 0
def __next__(self):
if self.index == len(self.words):
raise StopIteration()
word = self.words[self.index]
self.index += 1
return word
def __iter__(self):
return self
```
本示例定義了一個`CapitalIterable`類,其任務是循環遍歷
字符串中的單詞,并大寫第一個字母輸出。可迭代程序的大部分工作被傳遞給`CapitalIterator`實現。與這個迭代器交互的典型方式如下:
```
>>> iterable = CapitalIterable('the quick brown fox jumps over the lazy
dog')
>>> iterator = iter(iterable)
>>> while True:
... try:
... print(next(iterator))
... except StopIteration:
... break
...
The
Quick
Brown
Fox
Jumps
Over
The
Lazy
Dog
```
這個例子首先構造一個可迭代對象,進而生成一個迭代器。兩者之間的區別可能需要解釋;可迭代對象是一個對象,其元素可以被循環歷遍。通常,這些元素可能會被循環多次,即使同時循環或有重疊的代碼在循環。另一方面,迭代器,表示可迭代對象的特定位置(譯注:臨時可消耗的對象);迭代器中有些項目已經被消耗掉了,有些還沒有。兩個不同的迭代器可能在單詞列表中的不同位置,但是任何一個迭代器只能標記唯一一個位置。
</b>
每次迭代器調用`next()`時,它都會從可迭代對象按順序返回另一個標記。最終,迭代器將耗盡(不再有任何元素返回),在這種情況下,停止迭代被引發,并且我們脫離循環。
</b>
當然,我們已經知道一個更簡單的從iterable構造迭代器的語法:
```
>>> for i in iterable:
... print(i)
...
The
Quick
Brown
Fox
Jumps
Over
The
Lazy
Dog
```
正如你所看見的,在`for`語句中,盡管看不到可怕的面向對象,但它實際上是面向對象設計原則的快捷方式。記住這一點,我們還將在解析中討論,它們同時也是面向對象工具的反極點(譯注:反極點翻譯的不好)。然而,它們使用同樣的迭代協議,是另外一種快捷方式。
## 解析
解析雖然簡單,但有力,解析語法允許我們在一行代碼里轉化或迭代一個可迭代對象。結果對象可以是一個普通的列表、集合或字典,或是一個更有效率的生成器表達器。
### 列表解析
列表解析是Python中最常使用的最有用的工具之一,人們傾向于將它作為高級工具。其實它并不是什么高級工具。實際上,我已經在之前的例子中展示了解析工具,并假設你們已經知道解析工具,雖然高級程序員用解析工具用得比較多,但它們是平凡的工具,在軟件開發中處理一些最常見的情況。
</b>
讓我們開一個最常見的操作:將列表元素轉換為一個相關列表元素。特別的,讓我們假設我們剛剛從一個文件讀取了一個字符串列表,現在我們向把它轉化為整數列表。我們知道列表元素都是整數,我們想在這些數字上做一些事情(例如,計算平均值),這里有一個簡單的方法:
```
input_strings = ['1', '5', '28', '131', '3']
output_integers = []
for num in input_strings:
output_integers.append(int(num))
```
只有三行代碼,而且做的還不錯。如果你還不習慣用解析,你甚至都不覺著它很丑陋!現在,看一看用列表解析完成同樣的工作:
```
input_strings = ['1', '5', '28', '131', '3']
output_integers = [int(num) for num in input_strings]
```
我們把代碼壓縮到只有一行,更重要的是,我們省去了對列表元素調用`append`方法。總體而言,這句代碼非常簡單地告訴我們正在發生什么,即使你還沒有習慣解析語法。
</b>
中括號意味著,我們新建的仍然是一個列表。這個列表中,是一個對輸入序列元素的`for`循環迭代。唯一有些令人困惑的是在列表開始括號和`for`循環開始之間到底發生了什么。其實這些表明正在對輸入列表*每個*元素所做的事情。這些元素指代是循環的`num`變量。它將每個`num`變量的元素轉換為`int`數據類型。
</b>
這就是列表解析。一點都不高級。解析是高度優化的C代碼;對于元素數量較大的循環迭代,列表解析的速度遠快于`for`循環。如果可讀性不是唯一盡可能使用它們的原因,那么速度肯定是。
</b>
將列表元素轉換為一個相關的列表,并不是列表解析唯一能做的事情,我們還可以在解析中加`if`語句,用于排除某些值。例如:
```
output_ints = [int(n) for n in input_strings if len(n) < 3]
```
我縮短了`num`變量的名稱,改為`n`,結果變量改為`output_ints`,這樣代碼長度還能放在一行里。除此之外,和之前例子的區別是加了一個`if len(n) < 3`條件。這個多出來的代碼將去除多于兩個字符的字符串。這個`if`語句將在`int`函數之前執行,它將先檢查字符串的長度。由于我們的輸入字符串表示的都是數字,它將把數字大于99的字符串排除掉。這些就是列表解析的全部!我們使用它們將輸入值映射到輸出值,使用過濾包括或去除滿足特定條件的值。
</b>
任何可迭代對象都可以輸入到列表解析中;任何包含在`for`循環中的部分都可以放在解析中。例如,文本文件是可迭代的;在文件迭代器上每調用一次`__next__`,將返回文件的一行。我們也可以加載含有制表符分隔的文件,使用`zip`函數將頭部第一行放入一個字典中:
```
import sys
filename = sys.argv[1]
with open(filename) as file:
header = file.readline().strip().split('\t')
contacts = [
dict(
zip(header, line.strip().split('\t'))
) for line in file
]
for contact in contacts:
print("email: {email} -- {last}, {first}".format(
**contact))
```
這一次,我加了一些空格,這樣可讀性更好一些(列表解析不必只寫在一行里)。這個例子根據壓縮頭部和從文件切分的每一行,創造了一個字典列表。
</b>
嗯,這都是些什么呢?不要擔心代碼或解釋是否有意義;它確實挺令人迷惑的。這里一個列表解析做了很多事情,這段代碼很難理解、很難讀,當然最終也很難維護。這個例子表明,列表解析并不是總是最好的解決方案;大多數程序員都會承認`for`循環更加可讀。
> 記住:不能濫用我們提供的那些工具!永遠選擇適合我們工作的合適的工具,寫可維護的代碼。
### 集合和字典解析
解析不僅僅可以用在列表上,我們還可以使用帶有大括號類似的語法創建集合和字典。讓我們從集合開始。一種創建集合的方法是,將列表解析放入`set()`結構體中,然后將它轉換為集合。但這樣做,將浪費內存創建中間列表,隨后又丟掉這個中間列表,為什么不能直接創建一個集合呢?
</b>
下面這個例子,我們使用一個命名元組給`author/title/genre`三元組建模,然后從中提取符合特定`genre`的作家元組:
```
from collections import namedtuple
Book = namedtuple("Book", "author title genre")
books = [
Book("Pratchett", "Nightwatch", "fantasy"),
Book("Pratchett", "Thief Of Time", "fantasy"),
Book("Le Guin", "The Dispossessed", "scifi"),
Book("Le Guin", "A Wizard Of Earthsea", "fantasy"),
Book("Turner", "The Thief", "fantasy"),
Book("Phillips", "Preston Diamond", "western"),
Book("Phillips", "Twice Upon A Time", "scifi"),
]
fantasy_authors = {
b.author for b in books if b.genre == 'fantasy'}
```
集合解析代碼與demo比確實少了很多!如果我們用列表解析,Terry Pratchett 將出現兩次。這是因為集合將刪除重復的元素,我們運行一下:
```
>>> fantasy_authors
{'Turner', 'Pratchett', 'Le Guin'}
```
我們可以加入冒號創建一個字典解析。它把序列通過*key: value*對轉換為一個字典。例如,如果我們知道標題,在一個字典里迅速查找作者和流派,字典解析將很有用。我們可以通過字典解析將標題和其他對象之間建立映射:
```
fantasy_titles = {
b.title: b for b in books if b.genre == 'fantasy'}
```
現在我們有了字典,我們可以使用普通語法按標題進行查找。
</b>
總之,解析既不是Python的高級工具,也不是應該避免使用的非面向對象工具。它們僅僅是簡潔的、優化了的,用于從一個已經存在的序列創建一個列表、集合或字典的語法。
### 生成器解析
有時候,我們想處理一個新序列,但又不想在系統內存創建新的列表、集合或字典。如果我們在每次迭代只處理一個元素,又不關心創建一個最終的容器對象,那么創建容器的做法是在浪費內存。當每次只處理一個元素時,我們只需要任意時刻將當前對象存儲在內存中。但是當我們創建一個容器,所有的對象在被處理前都必須存儲在這個容器內。例如,考慮一個處理日志文件的程序。一個非常簡單的日志可能按下面的格式包含信息:
```
Jan 26, 2015 11:25:25 DEBUG This is a debugging message.
Jan 26, 2015 11:25:36 INFO This is an information method.
Jan 26, 2015 11:25:46 WARNING This is a warning. It could be
serious.
Jan 26, 2015 11:25:52 WARNING Another warning sent.
Jan 26, 2015 11:25:59 INFO Here's some information.
Jan 26, 2015 11:26:13 DEBUG Debug messages are only useful
if you want to figure something out.
Jan 26, 2015 11:26:32 INFO Information is usually harmless,
but helpful.
Jan 26, 2015 11:26:40 WARNING Warnings should be heeded.
Jan 26, 2015 11:26:54 WARNING Watch for warnings.
```
日志文件在web服務器、數據庫、e-mail服務器很流行,它可能包含了GB級的數據(我最近不得不為一個很差的系統清理了2TB的日志)。如果我們想處理日志中每一行,不要使用列表解析;它將創建一個包含日志每一行的列表,這會把它們都放入內存中,電腦可能會崩潰(看你用什么操作系統)。
</b>
如果我們在日志文件上使用`for`循環,我們可能在讀取下一行到內存中之前,每次只處理一行日志。如果我們可以使用解析語法得到同樣的效果不是更好嗎?
</b>
這就是生成器表達式所做的事情。它們使用和解析相同的語法,但是它們并不創造最終的容器對象。為了創建一個生成器表達式,使用`()`替換`[]`或`{}`。
</b>
下面代碼解析一個和之前例子格式相同的日志文件,然后輸出一個新的僅包含`WARNING`行的日志文件:
```
import sys
inname = sys.argv[1]
outname = sys.argv[2]
with open(inname) as infile:
with open(outname, "w") as outfile:
warnings = (l for l in infile if 'WARNING' in l)
for l in warnings:
outfile.write(l)
```
這段程序在命令行里使用了兩個文件名,使用生成器表達式過濾出警告部分(這里,使用了`if`語法,并沒有修改每一行),然后輸出到另外一個文件。運行這個示例,將輸出:
```
Jan 26, 2015 11:25:46 WARNING This is a warning. It could be
serious.
Jan 26, 2015 11:25:52 WARNING Another warning sent.
Jan 26, 2015 11:26:40 WARNING Warnings should be heeded.
Jan 26, 2015 11:26:54 WARNING Watch for warnings.
```
當然,對于這么少的輸入文件,使用列表解析仍然時安全的,但是當文件有幾百萬行時,生成器表達式在內存和速度上都將有巨大的影響。
</b>
生成器表達式在函數調用上被廣泛使用。例如,我們可以在生成器表達式上而不是列表上調用`sum`、`min`或 `max`函數,因為這些函數每次只處理一個對象。我們僅僅關心結果,而不是中間容器。
</b>
通常,盡可能使用生成器表達式。如果我們并不是需要一個列表、集合或字典,而是簡單地過濾或轉換一個序列上的元素,生成器表達式是最有效率的。如果我們需要知道列表的長度,或對結果排序,刪除重復項,或創建一個字典,我們還是使用解析語法比較好。
## 生成器
生成器表達式實際上也是一種解析;它們把更加高級(這次它真的是高級的!)的生成器語法壓縮成一行。生成器語法的好處是它看起來并不像面向對象的語法,但是我們再探索一下,就會發現它是一種創建對象的簡單的語法快捷方式。
</b>
讓我們進一步看一下日志文件例子。如果我們想從我們的輸出文件刪除`WARNING`列(因為它是多余的,這個文件僅包含警告),在不同程度可讀性上,我們有幾種選擇。我們可以用生成器表達式實現:
```
import sys
inname = sys.argv[1]
outname = sys.argv[2]
with open(inname) as infile:
with open(outname, "w") as outfile:
warnings = ( l.replace('\tWARNING', '')
for l in infile if 'WARNING' in l)
for l in warnings:
outfile.write(l)
```
可讀性已經相當好了,雖然我不想把表達式變得比這個更加復雜了。我們也可以用一般的`for`循環實現:
```
import sys
inname = sys.argv[1]
outname = sys.argv[2]
with open(inname) as infile:
with open(outname, "w") as outfile:
for l in infile:
if 'WARNING' in l:
outfile.write(l.replace('\tWARNING', ''))
```
雖然維護性比較好,但這么幾行代碼用了這么多縮進,實在有點丑陋。高能警告,如果我們想做點別的,而不是僅僅把它們打印出來,我們還得復制這些循環和條件。讓我們考慮一個真實的面向對象的解決方案,不用任何語法糖:
```
import sys
inname, outname = sys.argv[1:3]
class WarningFilter:
def __init__(self, insequence):
self.insequence = insequence
def __iter__(self):
return self
def __next__(self):
l = self.insequence.readline()
while l and 'WARNING' not in l:
l = self.insequence.readline()
if not l:
raise StopIteration
return l.replace('\tWARNING', '')
with open(inname) as infile:
with open(outname, "w") as outfile:
filter = WarningFilter(infile)
for l in filter:
outfile.write(l)
```
不要有所懷疑:就是如此丑陋,難讀到你可能都不知道它想干什么。我們創建了一個把文件對象作為輸入的對象,像任何迭代器那樣提供一個`__next__`方法。
</b>
`__next__`方法從文件中讀取每一行,同時丟掉不包含`WARNING`的行。當它遇到`WARNING`的行,則返回這一行。然后`for`循環再次調用`__next__`處理下一個`WARNING`行。當我們歷遍所有的行后,我們拋出一個`StopIteration`,告訴循環我們已經完成迭代。和其他例子比,它有點丑陋,但仍然很強大;既然我們手上有一個類,我們可以做一些我們想做的事情。
</b>
有了這些背景,讓我們最后看看生成器是什么樣子的。下面這個例子和我們之前的例子做相同的事情:創建一個帶有`__next__`方法的對象,當歷遍完輸入后拋出`StopIteration`。
```
import sys
inname, outname = sys.argv[1:3]
def warnings_filter(insequence):
for l in insequence:
if 'WARNING' in l:
yield l.replace('\tWARNING', '')
with open(inname) as infile:
with open(outname, "w") as outfile:
filter = warnings_filter(infile)
for l in filter:
outfile.write(l)
```
OK,可讀性還不錯,可能吧...至少代碼少了很多。但是究竟發生了什么,好像沒有新增什么有意義的。`yield`又是什么呢?
</b>
實際上,`yield`是生成器的關鍵。當Python看到函數中的`yield`,它會將這個函數包裝成一個對象,但這個對象并不同于我們之前例子中的對象。`yield`和`return`有點類似;都是退出函數并返回一行。不同點在于,但函數再次被調用時(通過`next()`),`yield`從它之前離開的下一行開始執行,而不是函數開始的位置。這里,函數在`yield`“之后”并沒有下一行,因此它跳到`for`循環的下一個迭代。因為`yield`語句中在一個`if`語句中,它將僅僅得到包含`WARNING`的行。
</b>
雖然看上去這仍然時一個歷遍各行的函數,實際上它創建了一個特殊類型的對象,一個生成器對象:
```
>>> print(warnings_filter([]))
<generator object warnings_filter at 0xb728c6bc>
```
我傳入一個空列表到這個函數作為一個迭代器。這個函數將創建和返回一個生成器對象。這個對象有`__iter__`和`__next__`方法,就像我們之前的例子里所創建的那樣。無論何時`__next__`被調用,生成器將運行函數直到它發現一個`yield`語句,它隨后從`yield`語句返回一個值,等到下一次`__next__`被調用,它從`yield`離開的位置執行。
</b>
生成器的這種用法并不算高級,但是如果你沒有意識到這個函數正在創建一個對象,它看起來會有點神奇。這個例子雖然很簡單,但你從中看到`yield`在單個函數中多次調用將產生強大的作用;生成器只是從`yield`最近的位置重新開始,并繼續下一個。
### 從另一個可迭代程序中產生項目
通常,當我們構建一個生成器函數,我們最終會遇到這樣一種情況,我們希望從另一個可迭代對象中產生數據,可能是我們在生成器中創建的一個列表解析或生成器表達式,或是從外部傳入函數的元素。這總是可能的,通過循環遍歷可迭代對象并單獨產生每個項目。然而,從Python3.3開始,Python開發者引入了一種新的更加優雅的語法。
</b>
讓我們稍微修改一下這個生成器例子,它將接受一個文件名,而不是一個行序列。這通常是不允許的,因為它將對象綁定在一個特定的范式上。可能的話,我們應該盡可能在作為輸入的迭代器上進行操作;通過這種方法,相同的函數將被使用,不管日志行是來自文件、內存或基于網絡的日志聚合器。所以下面的例子是出于教學的原因而設計的。
</b>
該版本的代碼說明了你的生成器可以在從另一個可迭代對象(在本例中是生成器表達式)產生信息之前進行一些基本設置:
```
import sys
inname, outname = sys.argv[1:3]
def warnings_filter(infilename):
with open(infilename) as infile:
yield from (
l.replace('\tWARNING', '')
for l in infile
if 'WARNING' in l
)
filter = warnings_filter(inname)
with open(outname, "w") as outfile:
for l in filter:
outfile.write(l)
```
這段代碼將前面示例中的`for`循環組合到生成器表達式中。請注意我將生成器表達式的三個子句(轉換、循環和過濾器)放在單獨的行上,使它們更易讀。還要注意,這種轉變沒有太大的幫助;之前帶有`for`循環的示例更易讀。
</b>
因此,讓我們考慮一個比它的替代方案更易讀的例子。構建一個從多個其他生成器生成數據的生成器可能會很有用。例如,`itertools.chain`函數從序列中的可迭代對象產生數據,直到對象全部用完。使用`yield from`語法可以很容易地實現這一點,所以讓我們考慮一個經典的計算機科學問題:歷遍普通樹。
</b>
A common implementation of the general tree data structure is a computer's ilesystem. Let's model a few folders and iles in a Unix ilesystem so we can use yield from to walk them effectively:
通用樹數據結構是計算機文件系統中的一個常見實現。讓我們在一個Unix文件系統中模擬幾個文件夾和文件,這樣我們就可以使用`yield from`來有效地遍歷它們:
```
class File:
def __init__(self, name):
self.name = name
class Folder(File):
def __init__(self, name):
super().__init__(name)
self.children = []
root = Folder('')
etc = Folder('etc')
root.children.append(etc)
etc.children.append(File('passwd'))
etc.children.append(File('groups'))
httpd = Folder('httpd')
etc.children.append(httpd)
httpd.children.append(File('http.conf'))
var = Folder('var')
root.children.append(var)
log = Folder('log')
var.children.append(log)
log.children.append(File('messages'))
log.children.append(File('kernel'))
```
設置代碼看起來工作量很大,但是在一個真實的文件系統中,它甚至還要更多。我們必須從硬盤上讀取數據,并將其組織到樹。然而,一旦進入內存,輸出文件系統中每個文件的代碼相當優雅:
```
def walk(file):
if isinstance(file, Folder):
yield file.name + '/'
for f in file.children:
yield from walk(f)
else:
yield file.name
```
如果這段代碼遇到一個目錄,它遞歸地要求`walk()`生成一個
從屬于每個子級所有文件的列表,然后生成所有這些數據及其
自己的文件名。在這個簡單的例子,如果只是遇到普通文件,它只是
產生文件名。
</b>
另外,不使用生成器解決前面的問題是很棘手的,這個問題是一個常見的面試問題。如果你這樣回答,準備好面試官將對你留下深刻印象,并惱怒你如此輕易地回答了它。他們可能會要求你解釋到底發生了什么?當然,如果你學到了這一章的原則,你不會有任何問題。
</b>
在編寫鏈式生成器時,`yield from`語法是一個有用的快捷方式,但是它更常用于不同的目的:通過協程傳送數據。我們將在第13章“并發”中看到許多這樣的例子,但是現在,讓我們先了解協程是什么。
## 協程
協程是非常強大的構造器,經常與生成器混淆。許多作者不恰當地將協程描述為“多了一點額外語法的生成器”。這是一個很容易犯的錯誤,就像在Python 2.5中,當協程被引入時,它們被表示為“我們向生成器語法添加了一個`send`方法”。還有一些更復雜的事實,當你在Python創建一個協程,返回的對象是一個生成器。差別實際上很微妙,看幾個例子后你會更更加清楚。
> 雖然Python中的協程與生成器語法關系密切,但它們只是在我們一直在討論的迭代器協議上存在特殊關系。即將發布的Python 3.5版本使協程成為真正獨立的對象,并將提供新的語法來使用它們。
要記住的另一件事是,協程很難理解。它們不常使用,你可以跳過這一部分,并且在Python中愉快地開發數年,都可能遇不到它們。有幾個庫廣泛使用協程(主要用于并發或異步編程),但它們通常是這樣編寫的,你可以在不理解它們是如何工作的情況下使用協程!因此,如果你在這部分感到困惑,不要絕望。
</b>
但是,學習了下面這些例子,你將不會感到困惑。這是一個最簡單的可能協程;它允許我們一直運行一個可以增加任意值的計數牌:
```
def tally():
score = 0
while True:
increment = yield score
score += increment
```
這段代碼看起來像不可能工作的黑魔法,所以在逐行描述之前,我們先了解一下它是如何工作的。這個簡單的對象將用在給棒球隊計分的應用上。每個隊都分配單獨的計數對象,并且他們的分數每半局可以隨著累積的運行次數增加。看看這個互動環節:
```
>>> white_sox = tally()
>>> blue_jays = tally()
>>> next(white_sox)
0
>>> next(blue_jays)
0
>>> white_sox.send(3)
3
>>> blue_jays.send(2)
2
>>> white_sox.send(2)
5
>>> blue_jays.send(4)
6
```
首先,我們構建兩個計數對象,每個隊一個。是的,它們看起來像函數,但是與上一節中的生成器對象一樣,事實上,這里的`yield`語句告訴python,花點力氣把這個簡單的函數轉變變成一個對象。
</b>
然后,我們對每個協程對象調用`next()`。就像在任何生成器上調用`next`一樣,也就是說,它執行每一行代碼,直到它
遇到`yield`語句,返回該位置的值,然后暫停,直到
下一個`next()`調用。
</b>
到目前為止,沒有什么新內容。但是回頭看看我們協程中的`yield`語句:
```
increment = yield score
```
與生成器不同,這個`yield`函數看起來應該返回一個值,并將它賦給一個變量。事實上,這正是正在發生的事情。協程仍然在`yield`語句處暫停,等待被另一個`next()`調用再次激活。
</b>
或者,正如你在交互式會話中看到的,我們調用了一個名為`send()`的方法。`send()`方法執行與`next()`完全相同的操作,除此之外,它將生成器推進到下一個`yield`語句。它還允許你從生成器外部傳遞值。該值被分配給`yield`語句的左邊。
</b>
令許多人真正困惑的是協程的發生順序:
* `yield`出現,生成器暫停
* `send()`在函數外部發生,將生成器喚醒
* 發送的值被分配到`yield`語句的左側
* 生成器繼續處理,直到遇到另一個`yield`語句
所以,在這個特定的例子中,在我們構造了協程并通過調用`next()`把它推進到`yield`語句,每次對`send()`的連續調用都會傳遞一個值添加到協程中,協程將該值添加到它的分數中,返回到`while`循環,并繼續處理,直到它再次遇到`yield`語句。`yield`語句返回一個值,該值成為最后調用`send()`的返回值。不要忘記:`send()`方法不只是向生成器發送一個值,它還從即將發生的`yield`語句返回這個值,就像
`next()`。這是生成器和協程之間的區別:生成器只生成值,而協程還可以消費它們。
</b>
> `next(i)`、`i.__next__()`和`i.send(value)`的行為和語法相當不直觀且令人沮喪。第一個是普通函數,第二種是特殊方法,最后一種是普通方法。但是三者都做同樣的事情:推動生成器直到產生一個值并暫停。此外,`next()`函數和相關聯的方法可以通過調用`i.send(None)`來模仿(譯注:`next(blue_jays)`和`blue_jays.send(None)`是一樣的)。
使用兩個不同的方法名是有價值的,因為它有助于我們代碼的讀者很容易看出他們是在與協程還是生成器交互。我發現一個事實,在一個例子中,它是一個函數調用,在另外的例子里,它又成了一種普通方法,這有點令人惱火。
### 返回日志解析
當然,前面的例子很容易用幾個整數變量寫代碼,對它們調用`x += increment`。讓我們看第二個例子,協程實際上為我們節省了一些代碼。這個例子有點簡單(教學原因),但是我在實際工作中必須解決的一個問題。事實上,它邏輯上遵循之前關于處理日志文件的討論是完全偶然的;這些例子是為這本書的第一版寫的,然而問題在四年后出現了!
</b>
Linux內核日志包含看起來和下面有些像但不完全像的行:
```
unrelated log messages
sd 0:0:0:0 Attached Disk Drive
unrelated log messages
sd 0:0:0:0 (SERIAL=ZZ12345)
unrelated log messages
sd 0:0:0:0 [sda] Options
unrelated log messages
XFS ERROR [sda]
unrelated log messages
sd 2:0:0:1 Attached Disk Drive
unrelated log messages
sd 2:0:0:1 (SERIAL=ZZ67890)
unrelated log messages
sd 2:0:0:1 [sdb] Options
unrelated log messages
sd 3:0:1:8 Attached Disk Drive
unrelated log messages
sd 3:0:1:8 (SERIAL=WW11111)
unrelated log messages
sd 3:0:1:8 [sdc] Options
unrelated log messages
XFS ERROR [sdc]
unrelated log messages
```
這有一大堆分散的內核日志消息,其中一些屬于硬盤。硬盤消息可能與其他消息穿插在一起,但是它們以可預測的格式和順序出現,已知序列號的特定驅動器與總線標識符(如0:0:0:0:0)是相關聯的,塊設備標識符(例如`sda`)與總線是相關聯的。最后,如果驅動器有一個損壞的文件系統,它可能會因XFS錯誤而失敗。
</b>
現在,給定日志文件,我們需要解決的問題是如何獲得任何有XFS錯誤的驅動器的序列號。數據中心技術人員稍后可能會使用這個序列號來識別和更換驅動器。
</b>
我們知道可以使用正則表達式來識別單獨的行,但是我們將我們不得不在遍歷這些行時更改正則表達式,因為我們將根據我們之前的發現尋找不同的東西。另一個困難的是,如果我們發現一個錯誤字符串,關于那條總線的信息包含該字符串,以及已經處理過了的總線上驅動器的序列號。這可以通過以相反的順序迭代文件來解決。
</b>
在看這個例子之前,請注意,基于協程的解決方案代碼非常少:
```
import re
def match_regex(filename, regex):
with open(filename) as file:
lines = file.readlines()
for line in reversed(lines):
match = re.match(regex, line)
if match:
regex = yield match.groups()[0]
def get_serials(filename):
ERROR_RE = 'XFS ERROR (\[sd[a-z]\])'
matcher = match_regex(filename, ERROR_RE)
device = next(matcher)
while True:
bus = matcher.send(
'(sd \S+) {}.*'.format(re.escape(device)))
serial = matcher.send('{} \(SERIAL=([^)]*)\)'.format(bus))
yield serial
device = matcher.send(ERROR_RE)
for serial_number in get_serials('EXAMPLE_LOG.log'):
print(serial_number)
```
這段代碼巧妙地將作業分成兩個獨立的任務。第一個任務是循環所有行,并獲得任何匹配給定正則表達式的行。第二項任務是與第一項任務互動,并指導它在任何給定時間搜索時應該使用什么樣的正則表達式。
</b>
先看看`match_regex`協程。記住,當它被創建時,它不執行任何代碼;相反,它只是創建一個協程對象。一旦建成,協程之外的人最終會調用`next()`來啟動代碼運行,此時它存儲兩個變量的狀態,`filename`和`regex`。隨后它讀取文件中的所有行,并反向遍歷它們。每一行都與傳入的正則表達式比較,直到它找到匹配項。當找到匹配后,協程從正則表達式中生成第一個組并等待。
</b>
在將來的某個時候,其他代碼將會發送一個新的搜索正則表達式。請注意,協程從不關心它正在嘗試使用什么正則表達式進行匹配;它只是遍歷行并將它們與正則表達式進行比較。決定提供什么正則表達式是別人的責任。
</b>
在這個例子里,其他人就是`get_serials`生成器。它不在乎
文件中的行,事實上它甚至沒有意識到它們。它做的第一件事就是從
`match_regex`協程構造器創建一個`matcher`對象,給它一個默認要搜索的正則表達式。它將協程推進到第一個`yield`并把它返回的值儲存起來。然后,它進入一個循環,指示`matcher`對象基于存儲的設備ID搜索總線ID,然后基于該總線ID搜索序列號。
</b>
在指示`matcher`對象找到另一個設備ID之前,它會把這個序列號推給外層循環,并重復該循環。
</b>
基本上,協程(`match _ regex`,因為它使用`regex = yield`語法)的工作是搜索文件中的下一個重要行,而生成器的(`get_serial
`,使用無賦值的`yield`語法)工作是決定哪一行是重要的。生成器有關于這個特定問題的信息,例如行以什么順序出現在文件中。另一方面,協程可以插入任何需要在文件中搜索給定正則表達式的問題。
### 關閉協程并拋出異常
普通生成器通過從內部拋出停止迭代(`StopIteration`)來發出它們退出的信號。如果我們將多個生成器鏈接在一起(例如,通過從另一個生成器內部迭代一個生成器),`StopIteration`異常將向外傳播。最終,它會進入一個`for`循環,通知循環是時候退出了。
</b>
協程通常不遵循迭代機制;而是通過協程獲取數據,直到遇到異常,數據通常被推入協程(使用`send`方法)。進行推送的實體通常是負責告知協程它已完成推送;它通過調用協程的`close`方法來完成這項工作。
</b>
調用時,`close()`方法將在協程正在等待一個值被發送進來時,拋出`GeneratorExit`異常。讓協程將`yield`語句包裝在`try...finally`代碼塊中,通常是個好政策,可以執行清理任務(例如關閉關聯的文件或套接字)。
</b>
如果我們需要在協同中引發異常,我們可以以類似的方式使用`throw()`方法。它接受帶有可選的值和回溯參數的異常類型
。當我們在一個協同過程中遇到異常,并且希望在臨近的協程中拋出異常,可以保持回溯時,后者很有用。
</b>
如果您要構建健壯的基于協同程序的庫,這兩個特性都是至關重要的,但是我們在日常編碼生活中不太可能遇到它們。
### 協程、生成器、函數之間的關系
我們已經看到協程在起作用,所以現在讓我們回到關于它們如何與生成器相關的討論。在Python中,就像經常發生的情況一樣,它們的區別非常模糊。事實上,所有的協程都是生成器對象,很多作者經常互換使用這兩個術語。有時,他們將協程描述為生成器的子集(只有從`yield`中返回值的生成器才被認為是協程)。正如我們在前面幾節中看到的,在Python中這在技術上是正確的。
</b>
然而,在理論計算機科學的更大范圍內,協程被認為是更普遍的原則,而生成器是協程的一種特殊類型。此外,一般函數是協程的另一個不同子集。
</b>
協程是一個例程,它可以讓數據在一個或多個點傳入,并在一個或多個點傳出。在Python中,數據傳入傳出的點是`yield`語句。
</b>
函數或子程序是最簡單的協程類型。當函數返回時,你可以在一個點傳入數據,在另一個點取出數據。雖然一個函數可以有多個返回語句,但是對于函數的任何給定調用,只能調用其中一個。
</b>
最后,生成器是一種協同程序,它可以在一個點傳遞數據,但可以在多個點傳遞數據。在Python中,數據將在一個yield語句中傳遞出去,但是不能將數據傳遞回來。如果你調用`send`,數據將被無聲地丟棄。
</b>
所以理論上,生成器是協程的類型,函數是協程的類型,并且有些協同既不是函數也不是生成器。這很簡單,呃?那為什么在python中感覺更復雜呢?
</b>
在Python中,生成器和協同程序都是使用如下語法構建的,就像我們正在構建一個函數。但是最終得到的對象根本不是函數;這
一種完全不同的對象。當然,函數也是對象。但是他們有不同的界面;函數是可調用的,返回值,生成器使用`next()`將數據取出,協程使用`send`推入數據。
</b>
[Python yield 使用淺析](https://www.runoob.com/w3cnote/python-yield-used-analysis.html):這里介紹了分塊讀取。
## 個案研究
## 摘要