Python可迭代對象、迭代器與生成器

本節課綱

  • 可迭代對象
  • 迭代器
  • 生成器
    Python中內置的序列,如list、tuple、str、bytes、dict、set、collections.deque等都是可迭代對象,但它們不是迭代器。迭代器可以被 next() 函數調用,并不斷返回下一個值。Python從可迭代的對象中獲取迭代器。迭代器和生成器都是為了惰性求值(lazy evaluation),避免浪費內存空間,實現高效處理大量數據。在Python 3中,生成器有廣泛的用途,所有生成器都是迭代器,因為生成器完全實現了迭代器接口。迭代器用于從集合中取出元素,而生成器用于"憑空"生成元素 。PEP 342 給生成器增加了 send() 方法,實現了"基于生成器的協程"。PEP 380允許生成器中可以return返回值,并新增了 yield from 語法結構,打開了調用方和子生成器的雙向通道

1. 可迭代的對象

可迭代的對象(Iterable)是指使用iter()內置函數可以獲取迭代器(Iterator)的對象。Python解釋器需要迭代對象x時,會自動調用iter(x),內置的iter()函數有以下作用:

  1. 檢查對象x是否實現了__iter__()方法,如果實現了該方法就調用它,并嘗試獲取一個迭代器
  2. 如果沒有實現__iter__()方法,但是實現了__getitem__(index)方法,嘗試按順序(從索引0開始)獲取元素,即參數index是從0開始的整數(int)。之所以會檢查是否實現__getitem(index)__方法,為了向后兼容
  3. 如果前面都嘗試失敗,Python會拋出TypeError異常,通常會提示'X' object is not iterable(X類型的對象不可迭代),其中X是目標對象所屬的類

具體來說,哪些是可迭代對象呢?

  • 如果對象實現了能返回迭代器__iter__()方法,那么對象就是可迭代的
  • 如果對象實現了__getitem__(index)方法,而且index參數是從0開始的整數(索引),這種對象也可以迭代的。Python中內置的序列類型,如list、tuple、str、bytes、dict、set、collections.deque等都可以迭代,原因是它們都實現了__getitem__()方法(注意: 其實標準的序列還都實現了__iter__()方法)

1.1. 判斷對象是否可迭代

從Python 3.4開始,檢查對象x能否迭代,最準確的方法是:調用iter(x)函數,如果不可迭代,會拋出TypeError異常。這比使用isinstance(x, abc.Iterable)更準確,因為iter(x)函數會考慮到遺留的__getitem__(index)方法,而abc.Iterable類則不會考慮

1.2. getitem()

下面構造一個類,它實現了__getitem__()方法。可以給類的構造方法傳入包含一些文本的字符串,然后可以逐個單詞進行迭代:

'''創建test.py模塊'''
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 __getitem__(self, index):
        return self.words[index]

    def __len__(self):  # 為了讓對象可以迭代沒必要實現這個方法,這里是為了完善序列協議,即可以用len(s)獲取單詞個數
        return len(self.words)

    def __repr__(self):
        return 'Sentence({})'.format(reprlib.repr(self.text))

測試Sentence實例能否迭代:

In [1]: from test import Sentence  # 導入剛創建的類

In [2]: s = Sentence('I love Python')  # 傳入字符串,創建一個Sentence實例

In [3]: s
Out[3]: Sentence('I love Python')

In [4]: s[0]
Out[4]: 'I'

In [5]: s.__getitem__(0)
Out[5]: 'I'

In [6]: for word in s:  # Sentence實例可以迭代
   ...:     print(word)
   ...:     
I
love
Python

In [7]: list(s)  # 因為可以迭代,所以Sentence對象可以用于構建列表和其它可迭代的類型
Out[7]: ['I', 'love', 'Python']

In [8]: from collections import abc

In [9]: isinstance(s, abc.Iterable)  # 不能正確判斷Sentence類的對象s是可迭代的對象
Out[9]: False

In [10]: iter(s)  # 沒有拋出異常,返回迭代器,說明Sentence類的對象s是可迭代的
Out[10]: <iterator at 0x7f82a761e5f8>

1.3. iter()

如果實現了__iter__()方法,但該方法沒有返回迭代器時:

In [1]: class Foo:
   ...:     def __iter__(self):
   ...:         pass
   ...:     

In [2]: from collections import abc

In [3]: f = Foo()

In [4]: isinstance(f, abc.Iterable)  # 錯誤地判斷Foo類的對象f是可迭代的對象
Out[4]: True

In [5]: iter(f)  # 使用iter()方法會拋出異常,即對象f不可迭代,不能用for循環迭代它
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-a2fd621ca1d7> in <module>()
----> 1 iter(f)

TypeError: iter() returned non-iterator of type 'NoneType'

Python迭代協議要求iter()必須返回特殊的迭代器對象。下一節會講迭代器,迭代器對象必須實現__next__()方法,并使用StopIteration異常來通知迭代結束

In [1]: class Foo:
   ...:     def __iter__(self):  # 其實是將迭代請求委托給了列表
   ...:         return iter([1, 2, 3])  # iter()函數從列表創建迭代器,等價于[1, 2, 3].__iter__()
   ...:     

In [2]: from collections import abc

In [3]: f = Foo()

In [4]: isinstance(f, abc.Iterable)
Out[4]: True

In [5]: iter(f)
Out[5]: <list_iterator at 0x7fbe0e4f2d30>

In [6]: for i in f:
   ...:     print(i)
   ...:     
1
2
3

1.4. iter()函數的補充

iter()函數有兩種用法:

  • iter(iterable) -> iterator: 傳入可迭代的對象,返回迭代器
  • iter(callable, sentinel) -> iterator: 傳入兩個參數,第一個參數必須是可調用的對象,用于不斷調用(沒有參數),產出各個值;第二個值是哨符,這是個標記值,當可調用的對象返回這個值時,觸發迭代器拋出 StopIteration 異常,而不產出哨符

下述示例展示如何使用iter()函數的第2種用法來擲骰子,直到擲出 1 點為止:

In [1]: from random import randint

In [2]: def d6():
   ...:     return randint(1, 6)
   ...:

In [3]: d6_iter = iter(d6, 1)  # 第一個參數是d6函數,第二個參數是哨符

In [4]: d6_iter  # 這里的 iter 函數返回一個 callable_iterator 對象
Out[4]: <callable_iterator at 0x473c5d0>

In [5]: for roll in d6_iter:  # for 循環可能運行特別長的時間,不過肯定不會打印 1,因為 1 是哨符
   ...:     print(roll)
   ...:
6
3
5
2
4
4

實用的示例: 逐行讀取文件,直到遇到空行或者到達文件末尾為止

with open('mydata.txt') as fp:
    for line in iter(fp.readline, '\n'):  # fp.readline每次返回一行
        print(line)

2. 迭代器

迭代是數據處理的基石。當掃描內存中放不下的數據集時,我們要找到一種惰性獲取數據項的方式,即按需一次獲取一個數據項。這就是迭代器模式(Iterator pattern)

迭代器是這樣的對象:實現了無參數的__next__()方法,返回序列中的下一個元素,如果沒有元素了,就拋出StopIteration異常。即,迭代器可以被next()函數調用,并不斷返回下一個值。

在 Python 語言內部,迭代器用于支持:

  • for 循環
  • 構建和擴展集合類型
  • 逐行遍歷文本文件
  • 列表推導、字典推導和集合推導
  • 元組拆包
  • 調用函數時,使用 * 拆包實參

2.1. 判斷對象是否為迭代器

檢查對象x是否為迭代器最好的方式是調用 isinstance(x, abc.Iterator)

In [1]: from collections import abc

In [2]: isinstance([1,3,5], abc.Iterator)
Out[2]: False

In [3]: isinstance((2,4,6), abc.Iterator)
Out[3]: False

In [4]: isinstance({'name': 'wangy', 'age': 18}, abc.Iterator)
Out[4]: False

In [5]: isinstance({1, 2, 3}, abc.Iterator)
Out[5]: False

In [6]: isinstance('abc', abc.Iterator)
Out[6]: False

In [7]: isinstance(100, abc.Iterator)
Out[7]: False

In [8]: isinstance((x*2 for x in range(5)), abc.Iterator)  # 生成器表達式,后續會介紹
Out[8]: True

Python中內置的序列類型,如list、tuple、str、bytes、dict、set、collections.deque等都是可迭代的對象,但不是迭代器; 生成器一定是迭代器

2.2. next()和iter()

標準的迭代器接口:

  • __next__(): 返回下一個可用的元素,如果沒有元素了,拋出StopIteration異常。調用next(x)相當于調用x.__next__()
  • __iter__(): 返回迭代器本身(self),以便在應該使用可迭代的對象的地方能夠使用迭代器,比如在for循環、list(iterable)函數、sum(iterable, start=0, /)函數等應該使用可迭代的對象地方可以使用迭代器說明: 如章節1所述,只要實現了能返回迭代器__iter__()方法的對象就是可迭代的對象,所以,迭代器都是可迭代的對象!

下面的示例中,Sentence類的對象是可迭代的對象,而SentenceIterator類實現了典型的迭代器設計模式:

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)  # 迭代協議要求__iter__返回一個迭代器

class SentenceIterator:
    def __init__(self, words):
        self.words = words
        self.index = 0

    def __next__(self):
        try:
            word = self.words[self.index]  # 獲取 self.index 索引位(從0開始)上的單詞。
        except IndexError:
            raise StopIteration()  # 如果 self.index 索引位上沒有單詞,那么拋出 StopIteration 異常
        self.index += 1
        return word

    def __iter__(self):
        return self  # 返回迭代器本身

2.3. next()函數獲取迭代器中下一個元素

除了可以使用for循環處理迭代器中的元素以外,還可以使用next()函數,它實際上是調用iterator.__next__(),每調用一次該函數,就返回迭代器的下一個元素。如果已經是最后一個元素了,再繼續調用next()就會拋出StopIteration異常。一般來說,StopIteration異常是用來通知我們迭代結束的:

with open('/etc/passwd') as fd:
    try:
        while True:
            line = next(fd)
            print(line, end='')
    except StopIteration:
        pass

或者,為next()函數指定第二個參數(默認值),當執行到迭代器末尾后,返回默認值,而不是拋出異常:

with open('/etc/passwd') as fd:
    while True:
        line = next(fd, None)
        if line is None:
            break
        print(line, end='')

2.4. 可迭代的對象與迭代器的對比

首先,我們要明確可迭代的對象迭代器之間的關系:Python從可迭代的對象中獲取迭代器

比如,用for循環迭代一個字符串'ABC',字符串是可迭代的對象for循環的背后會先調用iter(s)將字符串轉換成迭代器,只不過我們看不到:

In [1]: s = 'ABC'

In [2]: for char in s:
   ...:     print(char)
   ...:
A
B
C

如果沒有for循環,就不得不使用while循環來模擬:

In [3]: it = iter(s)  # 使用可迭代的對象s構建迭代器it

In [4]: while True:
   ...:     try:
   ...:         print(next(it))  # 不斷在迭代器上調用next函數,獲取下一個字符
   ...:     except StopIteration:  # 如果沒有字符了,迭代器會拋出StopIteration異常
   ...:         del it
   ...:         break
   ...:
A
B
C

StopIteration異常表明迭代器到頭了,Python語言內部會處理for循環和其它迭代上下文(如列表推導、元組拆包等)中的StopIteration異常

使用章節2.2中定義的Sentence類,演示如何使用iter()函數來構建迭代器,并使用next()函數依次獲取迭代器中的元素:

In [1]: from test import Sentence

In [2]: s = Sentence('Pig and Pepper')

In [3]: it = iter(s)  # 獲取迭代器

In [4]: it
Out[4]: <iterator at 0x4148650>

In [5]: next(it)  # 使用next()方法獲取下一個單詞
Out[5]: 'Pig'

In [6]: it.__next__()  # __next__()方法也能達到效果,但我們應該避免直接調用特殊方法
Out[6]: 'and'

In [7]: next(it)
Out[7]: 'Pepper'

In [8]: next(it)  # 沒有單詞了,因此迭代器拋出StopIteration異常
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-8-bc1ab118995a> in <module>()
----> 1 next(it)

StopIteration:

In [9]: list(it)  # 到頭后,迭代器就沒用了
Out[9]: []

In [10]: list(iter(s))  # 如果想再次迭代,要重新構建迭代器
Out[10]: ['Pig', 'and', 'Pepper']

總結:

  • 迭代器要實現__next__()方法,返回迭代器中的下一個元素
  • 迭代器還要實現__iter__()方法,返回迭代器本身,因此,迭代器可以迭代。迭代器都是可迭代的對象
  • 可迭代的對象一定不能是自身的迭代器。也就是說,可迭代的對象必須實現__iter__()方法,但不能實現__next__()方法

3. 生成器

在Python中,可以使用生成器讓我們在迭代的過程中不斷計算后續的值,而不必將它們全部存儲在內存中:

'''斐波那契數列由0和1開始,之后的費波那契系數就是由之前的兩數相加而得出,它是一個無窮數列'''
def fib():  # 生成器函數
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

g = fib()  # 調用生成器函數,返回一個實現了迭代器接口的生成器對象,生成器一定是迭代器
counter = 1
for i in g:  # 可以迭代生成器
    print(i)  # 每需要一個值時,才會去計算生成
    counter += 1
    if counter > 10:  # 只生成斐波那契數列前10個數值
        break

3.1. 生成器函數

只要 Python 函數的定義體中有 yield 關鍵字,該函數就是生成器函數。調用生成器函數時,會返回一個生成器(generator)對象。也就是說,生成器函數是生成器工廠

普通的函數與生成器函數在語法上唯一的區別是,在后者的定義體中有 yield 關鍵字

In [1]: def gen_AB():  # 定義生成器函數的方式與普通的函數無異,只不過要使用 yield 關鍵字
   ...:     print('start')
   ...:     yield 'A'
   ...:     print('continue')
   ...:     yield 'B'
   ...:     print('end')
   ...:

In [2]: gen_AB  # 生成器函數
Out[2]: <function __main__.gen_AB()>

In [3]: g = gen_AB()  # 調用生成器函數,返回一個生成器對象,注意:此時并不會執行生成器函數定義體中的代碼,所以看不到打印start

In [4]: g
Out[4]: <generator object gen_AB at 0x04CA74E0>

In [5]: next(g)  # 生成器都是迭代器,執行next(g)時生成器函數會向前,前進到函數定義體中的下一個 yield 語句,生成 yield 關鍵字后面的表達式的值,在函數定義體的當前位置暫停,并返回生成的值
start
Out[5]: 'A'

In [6]: next(g)
continue
Out[6]: 'B'

In [7]: next(g)
end
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-7-e734f8aca5ac> in <module>()
----> 1 next(g)

StopIteration:

調用生成器函數后會創建一個新的生成器對象,但是此時還不會執行函數體。

第一次執行next(g)時,會激活生成器生成器函數會向前 前進到 函數定義體中的 下一個 yield 語句生成 yield關鍵字后面的表達式的值,在函數定義體的當前位置暫停,并返回生成的值。具體為:

  • 執行print('start')輸出start
  • 執行yield 'A',此處yield關鍵字后面的表達式為'A',即表達式的值為A。所以整條語句會生成值A,在函數定義體的當前位置暫停,并返回值A,我們在控制臺上看到輸出A

第二次執行next(g)時,生成器函數定義體中的代碼由 yield 'A' 前進到 yield 'B',所以會先輸出continue,并生成值B,又在函數定義體的當前位置暫停,返回值B

第三次執行next(g)時,由于函數體中沒有另一個 yield 語句,所以前進到生成器函數的末尾,會先輸出end。到達生成器函數定義體的末尾時,生成器對象拋出StopIteration異常

注意用詞: 普通函數返回值,調用生成器函數返回生成器,生成器產出或生成值

調用生成器函數后,會構建一個實現了迭代器接口的生成器對象,即,生成器一定是迭代器!

In [8]: for c in gen_AB():
   ...:     print('-->', c)
   ...:
start
--> A
continue
-->
end

所以,可以使用生成器函數改寫前面章節中的Sentence類,此時不再需要SentenceIterator類:

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 iter(self.words)  # 等價于self.words.__iter__()
        for word in self.words:
            yield word  # 產出當前的word

迭代器和生成器都是為了惰性求值(lazy evaluation),避免浪費內存空間。而上面的Sentence類卻不具備惰性,因為RE_WORD.findall(text)會創建所有匹配項的列表,然后將其綁定到 self.words 屬性上。如果我們傳入一個非常大的文本,那么該列表使用的內存量可能與文本本身一樣多,而假設我們只需要迭代前幾個單詞,那么將浪費大量的內存。

re.finditer 函數是 re.findall 函數的惰性版本,返回的不是列表,而是一個迭代器,按需生成 re.MatchObject實例。如果有很多匹配, re.finditer 函數能節省大量內存。我們要使用這個函數讓 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):
        # finditer()函數構建一個迭代器,包含 self.text 中匹配 RE_WORD 的單詞,產出 MatchObject 實例
        for match in RE_WORD.finditer(self.text):
            yield match.group()  # match.group() 方法從 MatchObject 實例中提取匹配正則表達式的具體文本

3.2. 生成器表達式

簡單的生成器函數(有yield關鍵字),可以替換成生成器表達式(沒有yield關鍵字,將列表推導中的[]替換為()即可),讓代碼變得更簡短

生成器表達式可以理解為列表推導的惰性版本:不會迫切地構建列表,而是返回一個生成器,按需惰性生成元素。也就是說,如果列表推導是制造列表的工廠,那么生成器表達式就是制造生成器的工廠:

In [1]: def gen_AB():
   ...:     print('start')
   ...:     yield 'A'
   ...:     print('continue')
   ...:     yield 'B'
   ...:     print('end')
   ...:

In [2]: res1 = [x*3 for x in gen_AB()]  # 列表推導迫切地迭代 gen_AB() 函數生成的生成器對象產出的元素: 'A' 和 'B'。注意,下面的輸出是 start、 continue 和 end
start
continue
end

In [3]: for i in res1:  # 這個 for 循環迭代列表推導生成的 res1 列表
   ...:     print('-->', i)
   ...:
--> AAA
--> BBB

In [4]: res2 = (x*3 for x in gen_AB())  # 把生成器表達式返回的值賦值給 res2。只需調用 gen_AB() 函數,雖然調用時會返回一個生成器,但是這里并不使用

In [5]: res2  # res2 是一個生成器對象
Out[5]: <generator object <genexpr> at 0x04599330>

In [6]: for i in res2:  # 只有 for 循環迭代 res2 時, gen_AB 函數的定義體才會真正執行。 for 循環每次迭代時會隱式調用 next(res2),前進到 gen_AB 函數中的下一個 yield 語句。注意, gen_AB 函數的輸出與 for 循環中 print 函數的輸出夾雜在一起
   ...:     print('-->', i)
   ...:
start
--> AAA
continue
--> BBB
end

可以看出,生成器表達式會產出生成器,因此可以使用生成器表達式進一步減少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):  # 不再是生成器函數了(沒有 yield),而是使用生成器表達式構建生成器
        return (match.group() for match in RE_WORD.finditer(self.text))

何時使用生成器表達式

生成器表達式是創建生成器的簡潔語法,這樣無需先定義函數再調用。不過,生成器函數靈活得多,可以使用多個語句實現復雜的邏輯,也可以作為協程使用(后續博文介紹)

遇到簡單的情況時,可以使用生成器表達式,因為這樣掃一眼就知道代碼的作用。如果生成器表達式要分成多行寫,我傾向于定義生成器函數,以便提高可讀性。此外,生成器函數有名稱,因此可以重用

如果將生成器表達式傳入只有一個參數的函數時,可以省略生成器表達式外面的()

In [1]: list((x*2 for x in range(5)))
Out[1]: [0, 2, 4, 6, 8]

In [2]: list(x*2 for x in range(5))  # 可以省略生成器表達式外面的()
Out[2]: [0, 2, 4, 6, 8]

3.3. 嵌套的生成器

可以將多個生成器管道(pipeline)一樣鏈接起來使用,更高效的處理數據:

In [1]: def integers():  # 1\. 產出整數的生成器
   ...:     for i in range(1, 9):
   ...:         yield i
   ...:         

In [2]: chain = integers()

In [3]: list(chain)
Out[3]: [1, 2, 3, 4, 5, 6, 7, 8]

In [4]: def squared(seq):  # 2\. 基于整數的生成器,產出平方數的生成器
   ...:     for i in seq:
   ...:         yield i * i
   ...:         

In [5]: chain = squared(integers())

In [6]: list(chain)
Out[6]: [1, 4, 9, 16, 25, 36, 49, 64]

In [7]: def negated(seq):  # 3\. 基于平方數的生成器,產出負的平方數的生成器
   ...:     for i in seq:
   ...:         yield -i
   ...:         

In [8]: chain = negated(squared(integers()))  # 鏈式生成器,更高效

In [9]: list(chain)
Out[9]: [-1, -4, -9, -16, -25, -36, -49, -64]

由于上面各生成器函數的功能都非常簡單,所以可以使用生成器表達式進一步優化鏈式生成器

In [1]: integers = range(1, 9)

In [2]: squared = (i * i for i in integers)

In [3]: negated = (-i for i in squared)

In [4]: negated
Out[4]: <generator object <genexpr> at 0x7f2a5c09be08>

In [5]: list(negated)
Out[5]: [-1, -4, -9, -16, -25, -36, -49, -64]

3.4. 增強生成器

Python 2.5 通過了 PEP 342 -- Coroutines via Enhanced Generators ,這個提案為生成器對象添加了額外的方法和功能,其中最值得關注的是.send()方法

.__next__()方法一樣,.send()方法使生成器前進到下一個yield語句。不過,.send()方法還允許調用方把數據發送給生成器,即不管傳給.send()方法什么參數,那個參數都會成為生成器函數定義體中對應的yield表達式的值。也就是說,.send()方法允許在調用方生成器之間雙向交換數據,而.__next__()方法只允許調用方生成器中獲取數據

查看生成器對象的狀態:

可以使用 inspect.getgeneratorstate(...) 函數查看生成器對象的當前狀態:

  • 'GEN_CREATED': 等待開始執行
  • 'GEN_RUNNING': 正在被解釋器執行。只有在多線程應用中才能看到這個狀態
  • 'GEN_SUSPENDED': 在yield表達式處暫停
  • 'GEN_CLOSED': 執行結束
In [1]: def echo(value=None):
   ...:     print("Execution starts when 'next()' is called for the first time.")
   ...:     try:
   ...:         while True:
   ...:             try:
   ...:                 value = (yield value)  # 調用send(x)方法后,等號左邊的value將被賦值為x
   ...:             except Exception as e:
   ...:                 value = e
   ...:     finally:
   ...:         print("Don't forget to clean up when 'close()' is called.")
   ...:         

In [2]: g = echo(1)  # 返回生成器對象,此時value=1

In [3]: import inspect

In [4]: inspect.getgeneratorstate(g)
Out[4]: 'GEN_CREATED'

In [5]: print(next(g))  # 第一次要調用next()方法,讓生成器前進到第一個yield處,后續才能在調用send()方法時,在該yield表達式位置接收客戶發送的數據
Execution starts when 'next()' is called for the first time.
Out[5]: 1  # (yield value),產出value的值,因為此時value=1,所以打印1

In [6]: inspect.getgeneratorstate(g)
Out[6]: 'GEN_SUSPENDED'

In [7]: print(next(g))  # 第二次調用next()方法,相當于調用send(None),所以value = (yield value)中等號左邊的value將被賦值為None。下一次While循環,又前進到(yield value)處,產出value的值,因為此時value=None,所以打印None
None

In [8]: inspect.getgeneratorstate(g)
Out[8]: 'GEN_SUSPENDED'

In [9]: print(g.send(2))  # 直接調用send(2)方法,所以value = (yield value)中等號左邊的value將被賦值為2。下一次While循環,又前進到(yield value)處,產出value的值,因為此時value=2,所以打印2
2

In [10]: g.throw(TypeError, "spam")  # 調用throw()方法,將異常對象發送給生成器,所以except語句會捕獲異常,即value=TypeError('spam')。下一次While循環,又前進到(yield value)處,產出value的值,因為此時value=TypeError('spam'),所以打印TypeError('spam')
Out[10]: TypeError('spam')

In [11]: g.close()  # 調用close()方法,關閉生成器
Don't forget to clean up when 'close()' is called.

In [12]: inspect.getgeneratorstate(g)
Out[12]: 'GEN_CLOSED'

這是一項重要的 "改進",甚至改變了生成器的本性:像這樣使用的話,生成器就變身為基于生成器的協程

注意: 給已結束的生成器發送任何值,都將拋出StopIteration異常,且返回值(保存在異常對象的value屬性上)是None

In [13]: g.send(3)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-35-494d69d54622> in <module>()
----> 1 g.send(3)

StopIteration:

3.5. yield from

yield from 是在Python3.3才出現的語法。所以這個特性在Python2中是沒有的。

yield from 后面需要加的是可迭代對象,它可以是普通的可迭代對象,也可以是迭代器,甚至是生成器。

3.5.1. 簡單應用:拼接可迭代對象

我們可以用一個使用yield和一個使用yield from的例子來對比看下。

使用yield

# 字符串
astr='ABC'
# 列表
alist=[1,2,3]
# 字典
adict={"name":"wangbm","age":18}
# 生成器
agen=(i for i in range(4,8))

def gen(*args):
    for item in args:
        for i in item:
            yield i

new_list=gen(astr, alist, adict,agen)
print(list(new_list))
# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]

使用yield from

# 字符串
astr='ABC'
# 列表
alist=[1,2,3]
# 字典
adict={"name":"wangbm","age":18}
# 生成器
agen=(i for i in range(4,8))

def gen(*args):
    for item in args:
        yield from item

new_list=gen(astr, alist, adict, agen)
print(list(new_list))
# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]

由上面兩種方式對比,可以看出,yield from后面加上可迭代對象,他可以把可迭代對象里的每個元素一個一個的yield出來,對比yield來說代碼更加簡潔,結構更加清晰。

3.5.2. 復雜應用:生成器的嵌套

yield from 后面加上一個生成器后,就實現了生成的嵌套。

當然實現生成器的嵌套,并不是一定必須要使用yield from,而是使用yield from可以讓我們避免讓我們自己處理各種料想不到的異常,而讓我們專注于業務代碼的實現。

講解之前,首先要知道幾個概念:

1、調用方:調用委派生成器的客戶端(調用方)代碼 2、委托生成器:包含yield from表達式的生成器函數 3、子生成器:yield from后面加的生成器函數

你可能不知道他們都是什么意思,沒關系,來看下這個例子。

這個例子,是實現實時計算平均值的。 比如,第一次傳入10,那返回平均數自然是10. 第二次傳入20,那返回平均數是(10+20)/2=15 第三次傳入30,那返回平均數(10+20+30)/3=20

# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        count += 1
        total += new_num
        average = total/count

# 委托生成器
def proxy_gen():
    while True:
        yield from average_gen()

# 調用方
def main():
    calc_average = proxy_gen()
    next(calc_average)            # 預激下生成器
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0

if __name__ == '__main__':
    main()

委托生成器的作用是:在調用方與子生成器之間建立一個雙向通道

所謂的雙向通道是什么意思呢? 調用方可以通過send()直接發送消息給子生成器,而子生成器yield的值,也是直接返回給調用方。

你可能會經常看到有些代碼,還可以在yield from前面看到可以賦值。這是什么用法?

你可能會以為,子生成器yield回來的值,被委托生成器給攔截了。你可以親自寫個demo運行試驗一下,并不是你想的那樣。 因為我們之前說了,委托生成器,只起一個橋梁作用,它建立的是一個雙向通道,它并沒有權利也沒有辦法,對子生成器yield回來的內容做攔截。

為了解釋這個用法,還是用上述的例子,并對其進行了一些改造。

# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        if new_num is None:
            break
        count += 1
        total += new_num
        average = total/count

    # 每一次return,都意味著當前協程結束。
    return total,count,average

# 委托生成器
def proxy_gen():
    while True:
        # 只有子生成器要結束(return)了,yield from左邊的變量才會被賦值,后面的代碼才會執行。
        total, count, average = yield from average_gen()
        print("計算完畢!!\n總共傳入 {} 個數值, 總和:{},平均數:{}".format(count, total, average))

# 調用方
def main():
    calc_average = proxy_gen()
    next(calc_average)            # 預激協程
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0
    calc_average.send(None)      # 結束協程
    # 如果此處再調用calc_average.send(10),由于上一協程已經結束,將重開一協程

if __name__ == '__main__':
    main()

運行后,輸出

10.0
15.0
20.0
計算完畢!!
總共傳入 3 個數值, 總和:60,平均數:20.0

為什么要使用yield from

既然委托生成器,起到的只是一個雙向通道的作用,還需要委托生成器做什么?調用方直接調用子生成器不就好啦?

下面我們來一起探討一下,到底yield from 有什么過人之處,讓我們非要用它不可。

因為它可以幫我們處理異常

如果我們去掉委托生成器,而直接調用子生成器。那我們就需要把代碼改成像下面這樣,我們需要自己捕獲異常并處理。而不像使yield from那樣省心。

# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        if new_num is None:
            break
        count += 1
        total += new_num
        average = total/count
    return total,count,average

# 調用方
def main():
    calc_average = average_gen()
    next(calc_average)            # 預激協程
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0

    # ----------------注意-----------------
    try:
        calc_average.send(None)
    except StopIteration as e:
        total, count, average = e.value
        print("計算完畢!!\n總共傳入 {} 個數值, 總和:{},平均數:{}".format(count, total, average))
    # ----------------注意-----------------

if __name__ == '__main__':
    main()
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容