python生成器詳解

前言

  • 作為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”
可迭代的對(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)景。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開了第一次的黨會(huì),身份的轉(zhuǎn)變要...
    余生動(dòng)聽閱讀 10,925評(píng)論 0 11
  • 彩排完,天已黑
    劉凱書法閱讀 4,503評(píng)論 1 3
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來的情緒。表情可以傳達(dá)很多信息。高興了當(dāng)然就笑了,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,987評(píng)論 2 7

友情鏈接更多精彩內(nèi)容