1. 程式人生 > >生成器詳解教程以及協程詳解教程

生成器詳解教程以及協程詳解教程

前文

  相對於迭代器的“笨重”,生成器要來的更為優雅(關於迭代器的詳解可參考:迭代器詳解)。生成器是特殊的迭代器,實現惰性演算法。

生成器的建立

  生成器的實現很簡單,因為本質上就是一個函式,在函式裡定義了yield方法,就是生成器,舉例如下: 在這裡插入圖片描述   當定義了fun1()這個函式時,在裡面用到yield的關鍵字,就可以實現生成器,呼叫fun1()返回的是generator,某種程度上來說,把def改為gen來定義生成器會更清晰明瞭,但guido好像很討厭加入新的單詞,這在用到try…else…/for…else…的時候也體現的淋漓盡致,畢竟此時的else都是表示當try沒有出現異常或者for沒有被break時呼叫,用then

不是會更好嗎?   如果把yield改為print,此時就會直接列印1了;但在生成器裡,逢yield被掛起(這也就是為什麼被成為惰性演算法),然後會把yield後面的值返回,但是得用next()來喚醒生成器才能得到返回值,舉例如下: 在這裡插入圖片描述   如上所示,用next得到yield掛起的值,同時當next超過可以獲得的範圍時,就會報錯StopIteration,這點是迭代性的特徵,同時也是for…in…實現原理,如果我們用while來實現for…in…的話,程式碼如下:

l = [1, 2, 3] #定義列表來迭代

g = iter(l) #需要現將可迭代物件轉換為迭代器
while True:
    try:
        x = next(g)
        print(x)
    except:
        break

  截止目前,關於生成器的建立介紹完了,這邊歸納下yield的特點:

  • 使用了yield關鍵字的函式不再是函式,而是生成器。(使用了yield的函式就是生成器)
  • yield關鍵字有兩點作用:

1.儲存當前執行狀態(斷點),然後暫停執行,即將生成器(函式)掛起 2.將yield關鍵字後面表示式的值作為返回值返回,此時可以理解為起到了return的作用

  • 使用next()函式讓生成器從斷點處繼續執行,即喚醒生成器(函式)

生成器的優雅以及另一種建立方式

  瞭解過迭代器的同學都知道,用迭代器來實現斐波那契數列的話,需要定義__init__ 、__iter __、__next __等方法,程式碼量大並且顯得很笨重,如果用迭代器,你會發現原來可以這麼簡單,話不多說上程式碼:

def fib(x):
    n, num1, num2  = 0, 0, 1
    while n<x:
        yield num1
        num1, num2 = num2, num1+num2
        n += 1
 
for i in fib(10):
   print(i)

  因為儲存的是惰性演算法,用next呼叫過於麻煩,一般都會用for…in…來遍歷出答案,輸出結果就是0,1,1,2,3,5,8,13,21,34。和用迭代器來實現斐波那契數列相比,生成器簡直要輕鬆太多。   讓我們講下生成器的另一種建立方法,就是生成器表示式。熟悉python語法的都比較清楚,一般我們有列表推導式、字典推導式,卻唯獨少了元祖推導式,因為被生成器給佔用了。舉例如下: 在這裡插入圖片描述   呼叫next可以正常獲得結果: 在這裡插入圖片描述

生成器的應用

  生成器第一大應用是廣泛應用於協程,用yield來實現io阻塞時執行其他程式碼模組。這邊先介紹它另一個應用——>讀取大檔案。   假如計算機的執行記憶體是2G,但要讀取4G的檔案,如果一次性讀取的話,肯定無法讀取,報錯MemoryError,此時就可以通過限定讀取size,以及通過生成器的方式來一次次讀取,程式碼如下:

def read_file(f_obj, size=2048): #定義生成器讀取檔案函式,預設讀取2048B大小
    while True:
        data = f_obj.read(size)
        if not data:
            break
        yield data #通過yield每次返回對應的data

with open('1.txt', 'r', encoding='utf-8') as f:
    content = read_file(f)
    for i in content: #最後利用for..in..把所有資料都迭代出來
        print(i)  

  如上程式碼+註釋所示,只要在函式里加上yield就可以逐步讀取資料,最後再用for遍歷出來所有的資料。

用yield實現協程

  協程,也稱為微執行緒,是用來實現非同步IO的概念。這邊簡單介紹下各自概念:

  1. 程序:相當於一個個的應用程式,例如開啟酷狗播放器,就是打開了一個程序,需要計算機準備相應的資源(如記憶體等)和cpu排程;
  2. 執行緒:相比於程序要更小,比如在酷狗播放器裡要下載音樂、要播放音樂等,這些就都是執行緒來完成的,它是cpu排程的最小單位(不需要資源,資源由程序提供),對應在程式碼上面就是實現一個功能的程式碼塊。
  3. 協程:微執行緒,位於一個執行緒內的執行單位,相比於執行緒要更小,由於在一個執行緒內,所以無法支援並行(同時執行多個事件,對應於cpu多核的充分利用),僅支援併發(同時可以做多個事件,對應於cpu單核的應用)。
  4. 同步IO:在執行任務時碰到IO阻塞(如檔案讀取、網站訪問)時,cpu會等待阻塞結束才繼續執行。
  5. 非同步IO:在執行任務時碰到IO阻塞,cpu會保留此任務的上下文環境,然後先去執行其他任務,等該任務通知io結束時再轉回來執行該任務。
  6. 協程相對執行緒優勢:在實現多工時, 執行緒切換從系統層面遠不止儲存和恢復 CPU上下文這麼簡單。 作業系統為了程式執行的高效性每個執行緒都有自己快取Cache等等資料,作業系統還會幫你做這些資料的恢復操作。 所以執行緒的切換非常耗效能。但是協程的切換隻是單純的操作CPU的上下文,所以一秒鐘切換個上百萬次系統都抗的住。

  概念大致介紹完畢,接下來舉例介紹如何用yield來實現協程:

import time

def fun1():
    while True:
        time.sleep(0.2) #進行IO阻塞
        print("fun1....")
        yield

def fun2():
    while True:
        time.sleep(0.2) #進行IO阻塞
        print("fun2....")
        yield

def main():
    f1 = fun1()
    f2 = fun2()
    while True:
        next(f1)
        next(f2)

if __name__ == '__main__':
    main()

  如上程式碼,在fun1和fun2裡分別定義了yield關鍵字,此時兩個生成器定義ok,在main裡用next來呼叫,每呼叫一次返回一個結果,然後被yield掛起,轉而執行下一個程式碼塊,這就是典型的協程:碰到io阻塞時,掛起該任務,轉而執行其他任務。結果如下(不斷地重複fun1…,fun2…):

fun1....
fun2....
fun1....
fun2....
fun1....
fun2....
fun1....
fun2....
...

  僅僅用next來實現的話會顯得過於簡單,生成器除了next來喚醒,還有一個send函式,可以在喚醒生成器的同時傳入一個值,同時生成器裡也得定義一個變數來接收,如a=yield b,這個表示式就是a是send傳入的值,而b是生成器返回的值,用python裡典型的生產者和消費者的模型介紹如下:

def consumer():  #消費者生成器
    r = ''
    while True:
        n = yield r  #n為send傳入的值,r為返回給send的值
        if not n:
            return
        print('消費者消費 %s...' % n)
        r = '200 OK'
def produce(c): #生產者函式,每生產完就喚醒消費者進行消費
    c.send(None)
    n =0
    while n < 5:
        n=n+ 1
        print('生產者生產 %s...' % n)
        r = c.send(n)
        print('執行成功! return: %s' % r)
    c.close()
c = consumer()   #用c來接收生成器物件
produce(c)  #將消費者生成器傳入給生產者

  結果如下:

生產者生產 1...
消費者消費 1...
執行成功! return: 200 OK
生產者生產 2...
消費者消費 2...
執行成功! return: 200 OK
生產者生產 3...
消費者消費 3...
執行成功! return: 200 OK
生產者生產 4...
消費者消費 4...
執行成功! return: 200 OK
生產者生產 5...
消費者消費 5...
執行成功! return: 200 OK

  簡單介紹下執行步驟:

  • c.send(None)來喚醒消費者,send用於喚醒必須得傳入None,否則得用next來喚醒。消費者喚醒後執行到n=yield r掛起,因為r為空,所以n接收到空。
  • 此時執行r=c.send(n),而n=1,所以會把1傳給消費者,則在消費者那n=yield r被send喚醒,同時n也等於send傳入的n,接著執行到print語句,把"消費者消費 1…"打印出,再由while回到n=yield r,又被掛起,同時把r返回給生產者的r=c.send(n),所以生產者的r接收到消費者傳入的r值。
  • n=yield r/r=c.send(n),關於該模型得好好理解,才能夠更好地理解用yield生成器來實現生產者以及消費者。

總結

  關於生成器和協程的教程已結束。在python3中,往往會用gevent模組來實現協程(實際上也是封裝的yield來實現的),由於篇幅所限,本篇暫不介紹,感興趣的可以追加了解。