前言
- 作為python程序員,生成器以及協(xié)程是必不可少的話題。你可能在面試中會(huì)經(jīng)常遇到這樣的問題:說一說生成器和迭代器的區(qū)別?使用了哪些異步插件?講一講asyncio的用法以及原理?等等。當(dāng)然,能回答出這些問題只是初級(jí)目標(biāo),重要的是,我們是否深入掌握了這些內(nèi)容,是否在實(shí)際中能夠找到合適的方法處理異步問題。我依照《fluent pyhton》中的經(jīng)典例子,結(jié)合我自己的理解,由淺入深講解。本來只打算寫一篇文章的,但是寫著寫著發(fā)現(xiàn)內(nèi)容過多,只好拆分出來。
iter(...) 函數(shù)如何把序列變得可以迭代
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
# re.findall 函數(shù)返回一個(gè)字符串列表,里面的元素是正則表達(dá)式的全部非重疊匹配。
self.words = RE_WORD.findall(text)
def __getitem__(self, item):
return self.words[item]
def __len__(self):
return len(self.words)
def __repr__(self):
# reprlib.repr 這個(gè)實(shí)用函數(shù)用于生成大型數(shù)據(jù)結(jié)構(gòu)的簡(jiǎn)略字符串表示形式
# 默認(rèn)情況下,reprlib.repr 函數(shù)生成的字符串最多有 30 個(gè)字符
return "Sentence({})".format(reprlib.repr(self.text))
Sentence 實(shí)例測(cè)試
if __name__ == '__main__':
s = Sentence('"The time has come," the Walrus said,')
print(s)
for word in s:
print(word)
print(list(s))
output
Sentence('"The time ha... Walrus said,')
The
time
has
come
the
Walrus
said
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
- 通過測(cè)試說明Sentence實(shí)例可迭代,實(shí)現(xiàn)了序列協(xié)議,但是為什么可迭代呢?
序列可以迭代的原因:iter函數(shù)
- 解釋器需要迭代對(duì)象x時(shí),會(huì)自動(dòng)調(diào)用iter(x)。
- 內(nèi)置的 iter 函數(shù)有以下作用
- 1.檢查對(duì)象是否實(shí)現(xiàn)了
__iter__方法,如果實(shí)現(xiàn)了就調(diào)用它,獲取一個(gè)迭代器。 - 2.如果沒有實(shí)現(xiàn)
__iter__方法,但是實(shí)現(xiàn)了__getitem__方法,Python 會(huì)創(chuàng)建一個(gè)迭代器,嘗試按順序(從索引 0 開始)獲取元素。 - 如果嘗試失敗,Python 拋出 TypeError 異常,通常會(huì)提示“x object is not iterable”
- 1.檢查對(duì)象是否實(shí)現(xiàn)了
可迭代的對(duì)象與迭代器的對(duì)比
- 使用 iter 內(nèi)置函數(shù)可以獲取迭代器的對(duì)象。
如果對(duì)象實(shí)現(xiàn)了能返回迭代器的 __iter__ 方法,那么對(duì)象就是可迭代的。 - 所以
任何 Python 序列都可迭代的原因是,它們都實(shí)現(xiàn)了 __getitem__ 方法,標(biāo)準(zhǔn)的序列也都實(shí)現(xiàn)了__iter__方法。 - 從 Python 3.4 開始,檢查對(duì)象 x 能否迭代,最準(zhǔn)確的方法是:
調(diào)用 iter(x) 函數(shù),如果不可迭代,再處理 TypeError 異常 - 標(biāo)準(zhǔn)的迭代器接口有兩個(gè)方法
-
__next__:返回下一個(gè)可用的元素,如果沒有元素了,拋出 StopIteration異常。 -
__iter__:返回 self,以便在應(yīng)該使用可迭代對(duì)象的地方使用迭代器,例如在 for 循環(huán)中。
-
- 下面使用前面的Sentence類來說明如何
使用 iter(...) 函數(shù)構(gòu)建迭代器,以及如何使用 next(...)函數(shù)使用迭代器:
if __name__ == '__main__':
s = Sentence('hello world')
it = iter(s) # 構(gòu)建迭代器
print(it)
print(next(it))
print(next(it))
print(next(it))
- output
<iterator object at 0x0BC82230>
hello
world
Traceback (most recent call last):
File "xxx.py", line 33, in <module>
print(next(it))
StopIteration
- 可知next方法會(huì)不斷拿出迭代器中的元素,如果沒有元素,返回StopIteration異常。如果使用next迭代完成后想再次迭代,必須重新構(gòu)建迭代器,因?yàn)閚ext會(huì)拿出迭代器中的元素而不放回:
if __name__ == '__main__':
s = Sentence('hello world')
it = iter(s) # 構(gòu)建迭代器
print(it) # <iterator object at 0x0C242230>
print(next(it)) # hello
print(next(it)) # world
# print(next(it))
print(list(it)) # []
print(list(iter(s))) # 重新構(gòu)建迭代器生成的列表并打印 # ['hello', 'world']
- 根據(jù)以上可以總結(jié)迭代器的定義:
實(shí)現(xiàn)了無參數(shù)的 __next__ 方法,返回序列中的下一個(gè)元素;如果沒有元素了,那么拋出 StopIteration 異常。Python 中的迭代器還實(shí)現(xiàn)了 __iter__ 方法,因此迭代器也可以迭代。 - 下面根據(jù)Sentence類來實(shí)現(xiàn)標(biāo)準(zhǔn)的迭代器:
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
"""可迭代的對(duì)象"""
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __iter__(self):
return SentenceIterator(self.words) # 返回迭代器
def __repr__(self):
return "Sentence({})".format(reprlib.repr(self.text))
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
- 在 SentenceIterator 類中實(shí)現(xiàn)了
__iter__方法看似沒什么必要,不過必須這樣做。因?yàn)榈鲬?yīng)該實(shí)現(xiàn)__next__和__iter__兩個(gè)方法。可迭代的對(duì)象有個(gè) __iter__ 方法,每次都實(shí)例化一個(gè)新的迭代器;而迭代器要實(shí)現(xiàn) __next__ 方法,返回單個(gè)元素,此外還要實(shí)現(xiàn) __iter__ 方法,返回迭代器本身。 - 因此,
迭代器可以迭代,但是可迭代的對(duì)象不是迭代器。 - 各個(gè)迭代器要能維護(hù)自身的內(nèi)部狀態(tài), 每次調(diào)用 iter(my_iterable) 都新建一個(gè)獨(dú)立的迭代器。這就是為什么這個(gè)示例需要定義 SentenceIterator 類。由此,
可迭代的對(duì)象一定不能是自身的迭代器。也就是說,可迭代的對(duì)象必須實(shí)現(xiàn) __iter__ 方法,但不能實(shí)現(xiàn) __next__ 方法。 - 你可能會(huì)想,難道我們定義一個(gè)可迭代的對(duì)象必須還得新增個(gè)迭代器對(duì)象嗎,也就是說,我想要去掉SentenceIterator迭代器類,有沒有更好的實(shí)現(xiàn)方式?這樣,生成器就出來了。下面改寫
__iter__方法,使用生成器如下:
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 __iter__(self):
"""生成器函數(shù)"""
for word in self.words:
yield word # yield 可以簡(jiǎn)單理解為return
return
def __repr__(self):
return "Sentence({})".format(reprlib.repr(self.text))
- 這里的
__iter__方法是生成器函數(shù), 每次調(diào)用__iter__方法都會(huì)自動(dòng)創(chuàng)建迭代器,所以迭代器其實(shí)是生成器對(duì)象。這里可能有點(diǎn)繞,下面來詳細(xì)解釋下生成器的原理。 - 生成器函數(shù)的工作原理:
只要 Python 函數(shù)的定義體中有 yield 關(guān)鍵字,該函數(shù)就是生成器函數(shù)。調(diào)用生成器函數(shù)時(shí),會(huì)返回一個(gè)生成器對(duì)象。也就是說,生成器函數(shù)是生成器工廠。下面通過一個(gè)示例來說明生成器的行為。
def gen():
for i in range(3):
yield i
if __name__ == '__main__':
g = gen()
print(g)
for item in g:
print(item)
print(next(g))
- output
<generator object gen at 0x0C8ED300>
0
1
2
Traceback (most recent call last):
File "xxx.py", line 56, in <module>
print(next(g))
StopIteration
- gen函數(shù)在調(diào)用是返回一個(gè)生成器對(duì)象(generator ),這個(gè)生成器對(duì)象時(shí)迭代器,會(huì)生成傳給 yield 關(guān)鍵字的表達(dá)式的值,由于這里的g是迭代器,所以迭代完成后使用next方法獲取不到元素而報(bào)錯(cuò),除非重新生成一個(gè)迭代器才可以進(jìn)行迭代。
-
__iter__方法是生成器函數(shù),調(diào)用時(shí)會(huì)構(gòu)建一個(gè)實(shí)現(xiàn)了迭代器接口的生成器對(duì)象,因此不用再定義額外的迭代器類了。 - 遍歷列表會(huì)消耗不少內(nèi)存,特別是在列表比較大時(shí),而且如果我們只需要某幾個(gè)元素,重復(fù)遍歷列表顯然有點(diǎn)殺雞用牛刀。Sentence類中findall返回的是列表,能否直接返回迭代器呢?re.finditer就考慮到了這點(diǎn),re.finditer返回的不是列表,而是一個(gè)生成器,按需生成 re.MatchObject 實(shí)例。這樣
__iter__無需遍歷列表就可以直接獲取生成器實(shí)例,顯然能節(jié)省大量內(nèi)存。如下示例:
class Sentence:
"""可迭代的對(duì)象"""
def __init__(self, text):
self.text = text
def __iter__(self):
for match in RE_WORD.finditer(self.text): # finditer 函數(shù)構(gòu)建一個(gè)迭代器
yield match.group()
def __repr__(self):
return "Sentence({})".format(reprlib.repr(self.text))
- 想必python程序員經(jīng)常會(huì)用到列表推導(dǎo)式,但是生成器表達(dá)式可能不太常用,生成器表達(dá)式構(gòu)建一個(gè)生成器,但是會(huì)大大簡(jiǎn)化代碼。下面通過一個(gè)例子先來看看列表推導(dǎo)式和生成器表達(dá)式的區(qū)別:
def gen():
for i in range(3):
print(i)
yield str(i)
if __name__ == '__main__':
res1 = [x*3 for x in gen()] # 列表推導(dǎo)式
print('---------------------------')
res2 = (x*3 for x in gen()) # 生成器表達(dá)式
for item in res1:
print(item)
print('---------------------------')
for item in res2:
print(item)
- output
0
1
2
---------------------------
000
111
222
---------------------------
0
000
1
111
2
222
- 可以看出,列表推導(dǎo)式會(huì)直接迭代完迭代器,返回一個(gè)列表,而
生成器表達(dá)式返回一個(gè)生成器對(duì)象(res2),只有迭代這個(gè)生成器(res2)時(shí)才會(huì)執(zhí)行迭代器函數(shù)。for 循環(huán)迭代 res2 時(shí),實(shí)際上每次迭代時(shí)會(huì)隱式調(diào)用 next(res2),前進(jìn)到 gen 函數(shù)中的下一個(gè) yield 語句。 - 由于生成器表達(dá)式返回一個(gè)生成器對(duì)象,所以可以進(jìn)一步簡(jiǎn)化Sentence類:
class Sentence:
"""可迭代的對(duì)象"""
def __init__(self, text):
self.text = text
def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))
def __repr__(self):
"""使用生成器表達(dá)式返回生成器對(duì)象"""
return "Sentence({})".format(reprlib.repr(self.text))
- 生成器表達(dá)式是語法糖:完全可以替換成生成器函數(shù),不過有時(shí)使用生成器表達(dá)式更便利。
- 那么何時(shí)使用生成器表達(dá)式呢?和列表推導(dǎo)式一樣,如果生成器表達(dá)式要分成多行寫,建議定義生成器函數(shù),以便提高可讀性。在比較簡(jiǎn)單的情況下,生成器表達(dá)式可以代替列表推導(dǎo)式使用,這樣做不用立即返回列表從而大大減少內(nèi)存,在遇到大文件時(shí)尤其有用。
- 不過,生成器函數(shù)靈活得多,可以使用多個(gè)語句實(shí)現(xiàn)復(fù)雜的邏輯,也可以作為協(xié)程使用(后面說明)。
如果一個(gè)類只是為了構(gòu)建生成器而去實(shí)現(xiàn)__iter__ 方法,那還不如使用生成器函數(shù)。
yield from
- 如果生成器函數(shù)需要產(chǎn)出另一個(gè)生成器生成的值,傳統(tǒng)的解決方法是使用嵌套的 for 循環(huán):
def chain(*iterables):
for it in iterables:
for i in it:
yield i
if __name__ == '__main__':
a = "abc"
b = range(4)
print(list(chain(a, b))) # ['a', 'b', 'c', 0, 1, 2, 3]
python3.3引入了yield from,使用yield from可以改進(jìn) chain 生成器函數(shù):
def chain(*iterables):
for it in iterables:
yield from it
- 可以看出,yield from 完全代替了內(nèi)層的 for 循環(huán)。
除了代替循環(huán)之外,yield from 還會(huì)創(chuàng)建通道,把內(nèi)層生成器直接與外層生成器的客戶端聯(lián)系起來。把生成器當(dāng)成協(xié)程使用時(shí),這個(gè)通道特別重要,不僅能為客戶端代碼生成值,還能使用客戶端代碼提供的值。
深入iter
- 在 Python 中迭代對(duì)象 x 時(shí)會(huì)調(diào)用 iter(x)??墒牵琲ter 函數(shù)還有一個(gè)鮮為人知的用法:傳入兩個(gè)參數(shù),使用常規(guī)的函數(shù)或任何可調(diào)用的對(duì)象創(chuàng)建迭代器。第一個(gè)參數(shù)必須是可調(diào)用的對(duì)象,用于不斷調(diào)用(沒有參數(shù)),產(chǎn)出各個(gè)值;第二個(gè)值是哨符,這是個(gè)標(biāo)記值,當(dāng)可調(diào)用的對(duì)象返回這個(gè)值時(shí),觸發(fā)迭代器拋出 StopIteration 異常,而不產(chǎn)出哨符。看下面的例子就知道了:
def d1():
return randint(1, 6)
if __name__ == '__main__':
a = iter(d1, 1)
for i in a:
print(i)
- 無論怎樣運(yùn)行,都不打印1,當(dāng)可調(diào)用的對(duì)象返回為1時(shí),和第二個(gè)參數(shù)(哨符)相同,就會(huì)觸發(fā)StopIteration 異常,不產(chǎn)出哨符;也就是說,當(dāng)隨機(jī)數(shù)產(chǎn)出數(shù)字為哨符1 時(shí),for循環(huán)終止。
- 這樣的思想有很大用處,比如定時(shí)任務(wù)的終止回調(diào)、特定匹配回調(diào)等場(chǎng)景。