生成器 yield

因爲生成器的其中一種建立方式與列表推導式很類似,這裏先說一下列表推導式。html

列表推導式

列表推導式又叫列表生成式,官方叫作 list comprehension。顧名思義,這個是用來生成列表的。python

用法:express

[x for x in iterable]

通常狀況下,能夠用list()函數將序列轉換成列表,好比:app

>>> list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

上面的狀況比較單一,若是複雜一點,好比要生成[1x1, 2x2, 3x3, ..., 10x10],這個時候就只能循環了,列表推導式就是用來簡化這種狀況的。函數

>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

把要生成的元素x * x放在前面,後面跟for循環,就能夠把列表建立出來。運用列表推導式,能夠寫出很是簡潔的代碼。code

>>> L = ['Hello', 'World', 'IBM', 'Apple']
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']
生成器表達式

經過列表推導式,能夠直接生成一個列表,可是內存是有限的,若是一個列表太大,或者只須要訪問列表中的部分元素,那其他大部分元素佔用的空間就浪費了,生成器就是用來解決這個問題的。htm

生成器在訪問序列中的元素時,不是一下加載整個序列到內存,而是一邊循環一邊計算,也就是讀取一個元素計算一次,這樣若是序列很大就節省了大量空間。說白了,生成器就是一種特殊的迭代器。對象

生成器有兩種建立方法,第一種與列表推導式相似,只須要把列表推導式中的方括號[]改爲圓括號()便可建立一個生成器(generator)。內存

>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x0000000001E74CA8>

>>> for i in g:
...     print(i)
... 
0
1
4
9
16
25
36
49
64
81

建立生成器的第二種方法,是經過生成器函數。所謂生成器函數,就是在一個函數中包含yield語句。get

好比斐波那契函數:

>>> def fib(max):
        n, a, b = 0, 0, 1
        while n < max:
            yield b
            a, b = b, a + b
            n = n + 1
        return 'done'
... ... ... ... ... ... ...

帶有 yield 的函數再也不是一個普通函數,而是一個generator。

>>> fib(10)
<generator object fib at 0x0000000001DE6A40>

>>> list(fib(10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

須要注意的是,生成器函數和普通函數執行的流程是不同的。普通函數是遇到 return 語句或者執行結束就返回。而生成器函數是遇到 yield 就返回一個迭代值,再次執行時從上次返回的 yield 語句處繼續執行,看起來就像一個函數在正常執行的過程當中被 yield 中斷了數次,每次中斷都會經過 yield 返回當前迭代值。

好像說的有點繞,簡單來講,yield就是中斷函數並返回迭代值,並且中斷不會對函數變量形成影響即狀態不變,返回迭代值以後繼續執行函數。

再看看下面這個示例:

>>> def odd():
        print('step 1')
        yield 1
        print('step 2')
        yield(3)
        print('step 3')
        yield(5)

>>> odd()
<generator object odd at 0x0000000001DE6A98>

>>> for i in odd():
...     print(i)
... 
step 1
1
step 2
3
step 3
5

遇到yield就返回迭代值,這個返回相似return但不會退出函數(能夠理解爲掛起),而後繼續執行函數,直到再次遇到yield。

另外,上面生成器函數fib()最後的retrun的值沒有返回(在普通函數中是能夠返回的)。由於,調用生成器函數並無執行函數代碼,而是返回了一個generator object,而後再從generator object中迭代取值的時候纔會真正執行函數代碼,因此調用生成器函數的時候並無報異常並退出,return的值也沒有返回。

在生成器中,不管迭代有沒有完成,遇到return語句都會直接終止迭代並拋出StopIteration異常。。

這裏順便說一下迭代器,迭代器能夠經過內置函數next()獲取迭代器中的下一個元素,迭代完成時會報StopIteration異常。

>>> g = (x * x for x in range(5))
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

在 for 循環裏,無需處理 StopIteration 異常,循環會正常結束。可是在while循環中會拋出異常,這時,須要捕獲異常纔會繼續執行return語句,return的返回值包含在StopIteration的value中。

>>> g=fib(6)
>>> while True:
        x = next(g)
        print('g:', x)
... ... ... 
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
StopIteration: done

再來看上面的fib()函數的StopIteration異常捕獲:

>>> g = fib(6)
>>> while True:
        try:
            x = next(g)
            print('g:', x)
        except StopIteration as e:
            print('Generator return value:', e.value)
            break
... ... ... ... ... ... ... 
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done

另外一個示例,文件讀取:

若是直接對文件對象調用 read() 方法,會致使不可預測的內存佔用。好的方法是利用固定長度的緩衝區來不斷讀取文件內容。

若是讀取的文件很大,yield會分批返回,有效地避免了內存佔用問題,輕鬆實現文件讀取。

>>> def read_file(fpath): 
        BLOCK_SIZE = 1024 
        with open(fpath, 'rb') as f: 
            while True: 
                block = f.read(BLOCK_SIZE) 
                if block: 
                    yield block 
                else: 
                    return

>>> g = read_file('users.txt')
>>> for i in g:
...     print(i)
... 
b'keith1:18:110\r\nkeith2:19:111\r\nkeith3:20:112\r\nkeith4:21:113'

參考:
https://docs.python.org/3/library/stdtypes.html#lists
https://docs.python.org/3/reference/simple_stmts.html#the-yield-statement
https://docs.python.org/3/reference/expressions.html#yieldexpr

相關文章
相關標籤/搜索