Python協程筆記 - yield

生成器(yield)做爲協程

yield其實是生成器,在python 2.5中,爲生成器增長了.send(value)方法。這樣調用者可使用send方法對生成器發送數據,發送的數據在生成器中會賦值給yield左側的變量(若是有的話),能夠生成器能夠做爲協程使用。python

下面是一個使用生成器實現的,求平均值的函數函數

def averager1():
    """
    使用yield接收數值,並求平均值
    :return:
    """
    count = 0
    total = 0.0
    average = 0.0
    while True:
        value = yield average
        count += 1
        total += value
        average = total/count


avg1 = averager1()
# 預激活協程,程序執行到yield出暫停,產出average,輸出0.0
print(next(avg1))
# 0.00
# 向協程發送數字
print(avg1.send(10))
# 10.0
print(avg1.send(20))
# 15.0
print(avg1.send(30))
# 20.0

這裏yield能夠理解爲鏈接調用者和生成器的運輸小車,只不過運輸的不是貨物,而是數據。預激活生成器時,至關於調用者打電話給生成器,讓他把小車開到調用者這裏等待接收貨物(代碼執行到yield處暫停),這個時候,若是生成器有什麼貨物(數據)須要運輸給調用者,那麼能夠順帶把貨物捎帶過去(yield average 產出值),固然也有多是空車駛到調用者這裏(yield右側沒有產出任何變量)。學習

接着,調用者須要將貨物(數據)運輸給生成器,那麼就是從新讓小車把貨物運送給生成器(調用生成器的.send()方法)。生成器接收到yield小車運輸過來的貨物以後(value = yield),總之能夠開始生產、銷售或者其餘事情(求平均值)。當生成器這些事情都作完後,又從新將小車開回到調用者這一方,並暫停,等待接收調用者的下一個指令,如此往返。spa

逐行解讀上一個例子的代碼code

首先,調用生成器函數,建立一個生成器對象avg。而後對生成器對象進行預激活,這裏的預激活指的是讓生成器對象執行到yield除,而後會暫停。由於生成器對象只有在yield除暫停時,才能接收到調用者經過send方法發送給生成器的值,因此沒有進行預激活的生成器對象沒法正常工做。協程

咱們知道,python的賦值語句,是從右向左執行的,因此在這裏例子中,yield average會先執行,將average產出。對象

這個時候程序的控制器轉交給調用者,繼續執行print語句,因此print(next(avg))會輸出yield產出的值,即average,輸出0.00。blog

生成器的調用者繼續往下執行,調用生成器的send方法,將10發送給生成器對象。開頭說過,調用生成器對象的send方法,發送的數據,會賦值給yield左邊的變量。在這個例子中,value = yield average, 暫時先把yield右側的average忽略掉,只看value = yield,能夠理解爲,將yield獲取到了調用者經過send方法發送給生成器的值,而後把這個值賦值給左側的變量value,接着是簡單的計算平均值,這樣就實現了調用者給生成器對象發送數據。generator

注意生成器內部有個while的無限循環,生成器內部的代碼會繼續執行,直到再次遇到yield,程序暫停,再次把average的值產出,並將程序的控制器再次轉交回調用者。it

這個時候調用者繼續執行print語句,輸出average的值:10.0。後面的幾回send也是同樣的效果。

終止生成器對象的循環

以前的例子,while True會致使生成器對象無限循環,每次都會在yield除暫停,產出平均值average,並等待接收調用者再次經過send方法傳入的新值。

若是須要終止生成器對象的無限循環,能夠用三種方式:

  • 發送哨符終止循環

  • 調用生成器的.throw()方法終止循環

  • 調用生成器的.close()方法終止循環

發送哨符終止循環

從最簡單的發送哨符終止循環開始,簡單的說,就是發送一個特定的值給生成器對象,當生成器獲取到這個值時,就經過break語句退出while循環。

def averager2():
    """
    使用yield接收數值,並求平均值
    相對於上面的例子,增長了使協程退出的哨符
    :return:
    """
    count = 0
    total = 0.0
    average = 0.0
    while True:
        value = yield average
        # 當value爲None時,退出循環
        if value is None:
            break
        count += 1
        total += value
        average = total/count

avg2 = averager2()
# 預激活協程,由於yield右邊沒有變量,因此不會產出值
print(next(avg2))
# 0.0
# 向協程發送數字
print(avg2.send(10))
# 10.0
print(avg2.send(20))
# 15.0
print(avg2.send(30))
# 20.0
# 生成器循環終止時會拋出StopIteration
# 因此作一個異常捕獲
try:
    avg2.send(None)
except StopIteration:
    pass

上面的生成器函數,作了一個簡單的判斷,當value爲None時,就執行break語句退出生成器對象的循環。生成器循環終止時會拋出StopIteration,這個也會做爲後面生成器返回值的途徑。

調用.throw()方法終止循環

調用生成器的.throw()方法,會將異常發送給生成器,生成器的處理規則以下:

  1. 生成器在yield處暫停時,會接收到throw方法傳入的異常

  2. 若是生成器能正確處理傳入的異常,那麼生成器的代碼會繼續執行,yield產出右側的值(若是右側有值的話),並做爲調用者調用生成器.throw()方法的返回值

  3. 若是生成器不能處理傳入的異常,那麼生成器的代碼會停止運行,並將異常向上冒泡,再次發給調用者

看一個例子

# 對第一個函數averager1進行修改,增長處理ValueError的代碼
def averager3():
    """
    使用yield接收數值,並求平均值
    對第一個函數averager3進行修改,增長處理ValueError的代碼
    :return:
    """
    count = 0
    total = 0.0
    average = 0.0
    while True:
        try:
            value = yield average
            count += 1
            total += value
            average = total/count
        except ValueError:
            # 若是捕獲到ValueError,什麼都不作
            # 這樣生成器會繼續循環,直到再次遇到yield暫停
            pass


avg3 = averager3()
next(avg3)
print(avg3.send(10))
# 10.0
print(avg3.send(20))
# 15.0
# throw一個生成器能夠處理的異常ValueError,沒有任何影響
# 生成器會繼續運行,產出average,由於在yield處就報錯,後續的代碼沒有執行
# 因此average仍然爲15.0
# yield會將average產出,產出的值做爲調用者執行生成器的throw方法的返回值,最終輸出15.0
print(avg3.throw(ValueError))
# 15.0
# throw一個生成器不能處理的異常,生成器循環終止
try:
    print(avg3.throw(TypeError))
except TypeError:
    print('生成器沒法處理TypeError,異常向上冒泡拋出,循環終止')

調用.close()方法終止循環

close()方法,其實是讓生成器在yield出拋出GeneratorExit異常。

不過和直接.throw(GeneratorExit)不一樣的是,經過close讓生成器拋出GeneratorExit後,生成器不能再產出任何值,不然會引起RuntimeError: generator ignored GeneratorExit。

# 對第三個函數averager3進行修改,改成捕獲GeneratorExit異常並忽略
def averager4():
    """
    使用yield接收數值,並求平均值
    對第三個函數averager3進行修改,改成捕獲GeneratorExit異常並忽略
    :return:
    """
    count = 0
    total = 0.0
    average = 0.0
    while True:
        try:
            value = yield average
            count += 1
            total += value
            average = total/count
        except GeneratorExit:
            # 若是捕獲到GeneratorExit,什麼都不作
            # 這樣生成器會繼續循環,直到再次遇到yield
            # 由於調用close後不容許再次yield,因此會拋出
            # RuntimeError: generator ignored GeneratorExit
            pass
avg4 = averager4()
next(avg4)
print(avg4.send(10))
print(avg4.send(20))
avg4.close()
# RuntimeError: generator ignored GeneratorExit

若是是直接.throw(GeneratorExit),那麼遵循上述的規範,若是生成器處理了這個異常,循環繼續;若是生成器沒法的處理這個異常,循環終止。

一般狀況下,生成器不該該捕獲這個異常,或者捕獲這個異常後應拋出StopItreation異常,不然調用方會報錯。

協程返回值

協程是經過拋出StopIteration來返回值,StopIteration第一個值就是異常的返回值。

def averager5():
    """
    使用yield接收數值,並求平均值
    修改averager2,每次yield再也不產出平均數
    而是改成協程結束後再返回
    :return:
    """
    count = 0
    total = 0.0
    average = 0.0
    while True:
        value = yield
        # 當value爲None時,退出循環
        if value is None:
            break
        count += 1
        total += value
        average = total/count
    return average

avg5 = averager5()
next(avg5)
avg5.send(10)
avg5.send(20)
try:
    # 發送None,結束協程,同時捕獲StopIteration異常
    avg5.send(None)
except StopIteration as ex:
    print(ex)
    # 15

 

注:

《流暢的Python》學習筆記,部分例子來自書中,並有一些修改,便於驗證某些結論。

相關文章
相關標籤/搜索