所謂生成器

關於生成器,咱們能夠這樣理解:帶有 yield 的函數在 Python中被稱之爲 generator(生成器)。
生成器有關的說明以下:
(1)一個帶有 yield 的函數就是一個 generator,它和普通函數不一樣,生成一個 generator 看起來像函數調用,但不會執行任何函數代碼,直到對其調用 next()(在 for 循環中會自動調用 next())纔開始執行。

(2)雖然執行流程仍按函數的流程執行,但每執行到一個 yield 語句就會中斷,並返回一個迭代值,下次執行時從 yield 的下一個語句繼續執行。

(3)看起來就好像一個函數在正常執行的過程當中被 yield 中斷了數次,每次中斷都會經過 yield 返回當前的迭代值。

(4)事實上,在咱們建立了一個generator後,基本上永遠不會調用next(),而是經過for循環來迭代,而且不須要關心StopIteration的錯誤。

獲得一個生成器

獲得生成器的方式有兩種:一種是生成器表達式另一種是函數的方法

生成器表達式

其實,生成器表達式就是將列表推導式中的[]改爲()就能夠了:
generator = (i**2 for i in range(1,6))

函數的方法

利用函數的方法獲取一個生成器,這個函數中一定包含yield關鍵字:
def generator(max):
    while max > 0:
        yield max
        max -= 1

獲取生成器中的值

咱們能夠經過next()方法(或者__next__())以及for循環迭代獲取生成器中的值

next方法獲取生成器的值:

generator = (i for i in range(1,6))

print(next(generator))
print(next(generator))
print(next(generator))
print(next(generator))
print(next(generator))
# 生成器中只有5個數,超出範圍會拋出StopIteration異常
print(next(generator))
很明顯,咱們的生成器中只有5個值,可是取值的時候卻next了6次,因此在第六次視圖取值的時候,程序會拋出StopIteration異常

for循環迭代

而利用for循環的方法不用關心StopIteration異常:
generator = (i for i in range(1,6))

for num in generator:
    print(num,end=' ')
結果爲:
1 2 3 4 5

爲何要用生成器

生成器存在的最大的意義就是:節省內存空間
咱們來看一個Fibonacci數列的例子:一種佔用內存的作法是這樣的:
def fib(max):
    lst = []
    n,a,b = 0,0,1
    while n < max:
       lst.append(b)
       a,b = b,a+b
       n += 1
    return lst

f = fib(6)
print(f) # [1, 1, 2, 3, 5, 8]
也就是說,咱們把每一次的結果都append到一個列表中去了,最終將這個包含全部數據的列表返回。沒錯!聰明的你或許一眼就看出問題來了:若是這個max設置的特別大,那豈不意味着這個存放着全部數據的lst也會跟着增大,結果就會致使內存吃緊!
沒錯,生成器的存在就是爲了解決上面「大量數據佔用內存」的問題。
生成器解決上面問題的方法以下:
def fib(max):
    n,a,b = 0,0,1
    while n < max:
       yield b
       a,b = b,a+b
       n += 1

f = fib(6)
for i in f:
    print(i,end=' ') #1 1 2 3 5 8
對於生成器來講,它不會將函數產生的數據一次性的拿出來,而是在程序須要的時候,將數據一個一個的生產出來,相比於前面用列表一次性的將數據取出的方法,大大節省了程序對內存的佔用,而這也是生成器在實際中最經常使用的情景之一。

生成器的執行流程

對於生成器的執行流程,咱們用下面代碼來講明下:
def fib():
    a = b =1
    yield a
    yield b
    while 1:
        a , b = b , a+b
        yield b

g = fib()
for num in fib():
    if num > 10:break
    print(num)
這段代碼實際上是Fibonacci數列的另外一種實現方式。首先,咱們定義了一個生成器函數fib,而後將這個函數的執行結果賦值給g,也就是說,這裏的g就成爲了一個生成器。
當for循環開始遍歷(迭代)這個生成器的時候執行fib函數內部的代碼:第一句是將1賦值給a和b,接着遇到了yield a語句,當程序遇到yield語句時會暫時停下來,不執行後面的代碼,而此時,咱們在函數的外面就能夠經過next(生成器對象)的方法獲取當前yield後面的值(注意,for循環中自帶了next()方法),而咱們在for循環中獲得的第一個值就是當前的a的值1;接着,for循環開始遍歷第二個數(至關於執行第二個next(g))的時候,又發現了yield b,根據前面的說明,此時會打印第二個yield後面的b的當前值 1;在for循環進行第三次遍歷的時候進入while循環:首先將b的當前值賦值給a,而後將a+b的值賦值給b(Fibonacci數列的算法),而後遇到了第三個yield,所以第三次遍歷至關於執行了第三次next(g),因而此時會打印當前的b的值2,因此num的前三個值依次是:一、一、2。在第四次遍歷的時候,再次在while循環中進行數據的賦值與交換操做,直到獲得的值num不知足num>10這個條件爲止。
總的來說,其實就是函數在執行的過程當中只要遇到yield關鍵字就會中止,等待外面發出next(生成器對象)的信號再將yield後面的值返回。
下面的例子可能會更有助於你們理解:
咱們先打印一個next(g)看一下結果:
def count(n):
    while n > 0:
        print('before yield')
        yield n
        n -= 1
        print('after yield')

g = count(5)

print(next(g))
結果爲:
before yield
5
而後咱們打印兩次next(g)的時候看一下結果:
before yield
5
after yield
before yield
4
咱們能夠看到"after yield"實際上是在第二次執行next(g)的時候打印的,這也充分說明了第一次的時候count函數停在了yield n那裏。
從上面的例子咱們還能夠看到:如一個函數中出現多個yield則next()會中止在下一個yield前
def generator():
    print('one')
    yield 123
    print('two')
    yield 456
    print('end')

g = generator()
# 第一次運行,暫停在 yield 123,打印one與123
print(next(g))
# 第二次運行,暫停在 yield 456,打印two與456
print(next(g))
# 第三次運行,先打印end,可是因爲後面沒有yield語句了,所以再使用next()方法會報錯
print(next(g))
上面代碼的結論須要好好理解。

生成器中的return

關於生成器中用return,我的總結有邏輯結束顯示調用兩種

邏輯結束

所謂邏輯結束,其實就是咱們在設計程序的時候,在不知足一些條件的狀況下,直接使用return跳出函數:
def read_file(path): 
  size = 1024
  with open(path,'r') as f: 
    while True: 
      block = f.read(size) 
      if block: 
        yield block 
      else: 
        return
這種狀況下使用return其實是從程序的安全性考慮的,當咱們讀取一個文件的時候若是遇到空文件直接跳出函數,避免了read獲得的無效數據導致後續操做拋出異常。

顯示調用

從網上查看相關文檔,有這樣的說法:做爲生成器,由於每次迭代就會返回一個值,因此不能顯示的在生成器函數中return 某個值,包括None值也不行,不然會拋出「SyntaxError」的異常。可是本人在測試的時候發現這種狀況只在python2中會有,我本身用的python3.6.8解釋器並無報錯:

python2解釋器下的狀況:

Python 2.7.15 (v2.7.15:ca079a3ea3, Apr 30 2018, 16:30:26) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> def func():
...     yield 123
...     return 666
...
  File "<stdin>", line 3
SyntaxError: 'return' with argument inside generator

python3.6.8解釋器運行結果:

Python 3.6.8 (tags/v3.6.8:3c6b436a57, Dec 23 2018, 23:31:17) [MSC v.1916 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> def func():
...     yield 123
...     return 666
...
>>> g = func()
>>> print(next(g))
123

yield返回值與send方法

看下面代碼:
# -*- coding:utf-8 -*-
def func():
    print('one')
    yield 123
    print('two')
    yield 456
    print('end')

g = func()
print(next(g))
print(next(g))
結果爲:
one
123
two
456
這裏你可能會想:yield 123咱們打印出來123,yield 456打印出了456,那麼,123與456是否是yield的返回值呢?答案固然不是。
實際上,咱們獲取到的yield後面的值實際上是經過next()方法獲得的,而yield自己是有返回值的,默認狀況是None
咱們在上面的代碼基礎上作一些改動來看一下:
def func():
    print('one')
    a = yield 123
    print(a)
    print('two')
    yield 456
    print('end')

g = func()
print(next(g))
這裏咱們將第一個yield的返回值賦值給了a,接下來打印a。可是因爲程序運行到第一個yield的時候會停下來,不會接着執行,所以第一次不會打印a,結果爲:
one
123
而若是咱們在上面代碼的基礎上再加一個next(g)的話,打印的結果以下:
one
123
None
two
456
咱們會發如今進行到第二個yield的時候123與two之間打印出了None,這個None其實就是第一個yield的默認返回值
想要修改這個返回值,或者說爲其賦值的話,咱們就可使用send方法:經過send方法去爲上一次被掛起的yield語句賦值。
看下面的代碼:
def my_generator():
    value = yield 1
    value = yield(value)
    value = yield(value)

g = my_generator()

print(next(g))
print(g.send('hello'))
print(g.send('world'))
結果爲:
1
hello
world
具體的過程說明以下:
(1)當調用gen.next()方法時,python首先會執行MyGenerator方法的yield 1語句。因爲是一個yield語句,所以方法的執行過程被掛起,而next方法返回值爲yield關鍵字後面表達式的值,即爲1。

(2)當調用gen.send('hello')方法時,python首先恢復MyGenerator方法的運行環境。同時,將表達式(yield 1)的返回值定義爲send方法參數的值,即爲'hello'。
  這樣,接下來value=(yield 1)這一賦值語句會將value的值置爲'hello'。繼續運行會遇到yield value語句。所以,MyGenerator方法再次被掛起。
  同時,send方法的返回值爲yield關鍵字後面表達式的值,也即value的值,爲'hello'。

(3)當調用send('world')方法時MyGenerator方法的運行環境。同時,將表達式(yield value)的返回值定義爲send方法參數的值,即爲'world'。
  這樣,接下來value=(yield value)這一賦值語句會將value的值置爲'world'。第三次打印'world'。
能夠看到:能夠看出來:第一個的next取到了1;咱們把'hello'賦值給第一個yield做爲其返回值,因此第二次取到的是'hello',一樣的,第三次取到的是咱們爲第二個yield表達式send的返回值'world'。
總的來講,send方法和next方法惟一的區別是在執行send方法會首先把上一次掛起的yield語句的返回值經過參數設定,從而實現與生成器方法的交互。
可是須要注意,在一個生成器對象沒有執行next方法以前,因爲沒有yield語句被掛起,若是非要是用send方法,那麼這個在第一個位置的send方法裏面的參數必須是None,不然會報錯。
下面是錯誤的寫法:
def my_generator():
    value = yield 1
    value = yield(value)
    value = yield(value)

g = my_generator()

print(g.send('hello'))
print(g.send('world'))
程序會報這樣的錯:
TypeError: can't send non-None value to a just-started generator
若是非要在第一次使用send方法,正確的寫法是在send方法中加參數None:
def my_generator():
    value = yield 1
    value = yield(value)
    value = yield(value)

g = my_generator()

print(g.send(None))
print(g.send('hello'))
print(g.send('world'))
結果爲:z
1
hello
world
由於當send方法的參數爲None時,它與next方法徹底等價。可是注意,雖然上面的代碼能夠接受,可是不規範。因此,在調用send方法以前,仍是先調用一次next方法爲好。

利用yield實現簡單的協程案例——生產者消費者

這是yield十分關鍵的用處,理解了yield的機制對理解協程並進行相關併發的程序設計十分有幫助!
所謂協程,能夠簡單理解爲函數之間的相互切換。而利用yield與send方法咱們能夠十分方便的實現這種效果:
# -*- coding:utf-8 -*-
import time

def consumer():
    # consumer做爲一個生成器
    while 1:
        data = yield

def producer():
    # 生成器對象
    g = consumer()
    # 先next後面才能send具體的非None的值,至關於先send一個None
    next(g)
    for i in range(1000000):
        g.send(i)

if __name__ == '__main__':
    start = time.time()
    #併發執行,可是任務producer遇到io就會阻塞住,並不會切到該線程內的其餘任務去執行
    producer()
    print('執行時間:',time.time() - start)
結果爲:
執行時間: 0.12068915367126465
固然這涉及到了協程與IO阻塞相關的知識,咱們這裏不作討論,上述函數是爲了說明yield與send在函數任務之間不斷切換的功能
參考文獻:
https://www.cnblogs.com/wj-1314/p/8490822.html
https://blog.csdn.net/jason_cuijiahui/article/details/84947310
https://blog.csdn.net/zxpyld3x/article/details/79181834
https://blog.csdn.net/hedan2013/article/details/56293173
相關文章
相關標籤/搜索