在此前的文章中,我們介紹過迭代器模式
迭代器模式是一種十分常用的行為設計模式,各種面向對象編程語言大多提供了迭代器模式的實現和具體的工具類,迭代器主要用來按需要的順序順次獲取容器中的數據項。 我們在此前的文章中用簡單明了的例子說明了 Python 中迭代器與關鍵字 yield 的用法。
python yield 與生成器
他們就是我們本文詳細介紹的目標。
迭代器主要用於支持以下功能:
當遇到迭代的場景時,Python 解釋器會自動以該對象為參數調用內置的 iter 方法。 iter 方法執行邏輯如下:
上述第二步操作在設計中只是為了兼容舊版本,很可能會在未來被取消。 同時,只要對象 obj 實現了 __iter__ 方法,執行 isinstance(obj, abc.Iterable) 就會返回 True,這是因為 abc.Iterable 類中實現了我們此前介紹過的魔術方法 __subclasshook__
為什麼我們需要迭代器? 對於一個可迭代的對象,我們需要順次取出其中的元素,但對於序列、字典、元組甚至是樹、圖等結構的迭代,我們並不關心其數據結構的內部實現,我們只是需要其中取出的一個個元素,同時,對於某個特定的結構而言,可能存在多種迭代方式,例如對於二叉樹結構,有著先序、後序、中序等多種遍歷方式。 那麼,如何避免這些我們在順次迭代過程中並不關心的復雜性呢?使用統一的對象封裝,提供一套簡單、抽象的迭代方法是一個十分優雅的解決方案,這正是迭代器模式所做的。 迭代器通過操作被迭代對象,同時向上層客戶端提供通用的迭代使用方式,來實現對具體迭代細節的隱藏。
如果要讓你的對象可以被迭代,同時你又不可以保證你所實現的 __getitem__方法的 key 可以從 0 開始順次取出元素,那麼就必須實現 __iter__ 方法並返回一個 abc.Iterator 類的對象。 你返回的迭代器對象可以不顯式繼承 abc.Iterator,只要實現 __iter__ 和 __next__ 兩個方法,abc.Iterator 類的 __subclasshook__ 方法讓所有實現了這兩個方法的對象都可以通過 isinstance(obj, abc.Iterator) 返回 True。
用於創建並返回迭代器的方法。 通常,在一個可迭代對象中用來構建和返回所需要的迭代器類對象,而在迭代器類對象中,用來返回其自身的引用。
用於返回下一個迭代元素,如果已經完成迭代,則需拋出 StopIteration 異常,這也是 Python 迭代器設計思想中唯一能夠被感知到迭代完成的方法,循環、生成器、推導等多個場景中,解釋器均自動捕獲並處理了該異常。
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
return SentenceIterator(self.words)
class SentenceIterator:
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return word
def __iter__(self):
return self
實現 __iter__ 與 __next__ 兩個方法來實現一個迭代器類並不是 Python 習慣的做法,這看起來太過繁瑣了。 取而代之的,使用生成器函數則是更為簡潔和易於理解的。
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
for match in RE_WORD.finditer(self.text):
yield match.group()
只要函數定義體內有 yield 關鍵字,該函數就是一個生成器函數,其調用會返回一個生成器對象,也就是說,生成器函數是一個生成器工廠。
>>> def gen123():
... print('start')
... yield 1
... print('continue')
... yield 2
... print('end')
... yield 3
...
>>> g = gen123()
>>> next(g)
start
1
>>> next(g)
continue
2
>>> next(g)
end
3
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
>>> res = [x*3 for x in gen123()]
start
continue
end
>>> res
[3, 6, 9]
我們看到,生成器函數返回了一個生成器對象,這個生成器對象的行為與迭代器是完全一致的。
既然生成器函數是一個函數,那麼這個函數可以 return 某個值嗎? 在 python3.3 之前不可以,但在 python3.3 開始,python 引入了協程的概念,當把生成器函數當成協程來使用時,其 return 的結果才會具有意義,但即使如此,return 語句仍然會導致拋出 StopIteration 異常,但會攜帶 return 的值。 關於協程,我們此後會有一篇文章專門來介紹,敬請期待。
對於上面 Sentence 類的例子,還有另一種方法可以實現該類的迭代 — 生成器表達式。 有時使用生成器表達式更便利。
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))
生成器表達式是一個 python 中的語法糖,其本質上與生成器函數是一樣的,其與列表推導雖然在形式上十分相似。 但生成器表達式與列表推導有著本質上的不同,列表推導會一次性創建出所有的元素,如果列表中元素過多,則會導致內存占用的上升,而生成器函數、生成器表達式生成的生成器對象會通過記錄程序執行上下文,每次 next 調用只生成一個元素,從而實現內存的節約。 在大數據量的場景下,迭代器、生成器表達式、生成器函數是非常好的解決方案。
有時我們需要在我們的生成器函數中生成另一個生成器或迭代器的值。
>>> def geniter(iterable):
... for i in iterable:
... yield i
...
>>> g = geniter(range(3))
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
上面的例子中,我們通過循環的方式實現了通過另一個生成器實現我們的生成器函數的數據生成。 yield from 表達式讓我們可以省去代碼中的循環。
>>> def geniter(iterable):
... yield from iterable
...
>>> g = geniter(range(3))
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
yield from 並不只是一個語法糖,其與 python 中的協程密切相關,進一步的內容,敬請關注接下來關於 python 協程的文章。