天天看點

python協程系列(一)——生成器generator以及yield表達式詳解

  聲明:本文将詳細講解python協程的實作機理,為了徹底的弄明白它到底是怎麼一回事,鑒于篇幅較長,将徹底從最簡單的yield說起從最簡單的生成器開始說起,因為很多看到這樣一句話的時候很懵,即“yield也是一種簡單的協程”,這到底是為什麼呢?本次系列文章“python協程系列文章”将從最簡單的生成器、yield、yield from說起,然後詳細講解asyncio的實作方式。本文主要講解Python的生成器的各種詳細操作,以及yield表達式的詳細應用。

  一,生成器generator詳解

  注意:關于什麼是可疊代對象、什麼是生成器、什麼是疊代器這裡不再贅述。​

  yield是實作生成器的重要關鍵字,但是yield語句有一些非常重要的小細節需要注意,可能我們在寫一個簡單的生成器的時候有很多東西沒有用到,這裡将分情況逐一介紹。特别是生成器的三個重要方法,一個是next(),一個是send(),一個是throw(),他們到底有什麼樣的作用。

  1,最簡單的生成器

  test.py

# 最簡單的生成器
# 該生成器函數傳遞一個整數,然後周遊,生成器輸出為0,1,2,3...n-1
def my_generator(n):
    for i in range(n):
        yield i

for i in my_generator(5):
    print(i,end=' ')
# 0 1 2 3 4      
python協程系列(一)——生成器generator以及yield表達式詳解

   2,send()方法使用

# send()方法的使用
def my_generator(n):
    for i in range(n):
        temp = yield i
        print('我是%s,是通過send方法傳遞的參數' %(temp))

g = my_generator(5)
# 初始化生成器,相當于send(None)
# 如果使用send(None)初始化,第一次傳遞的參數必須為None
# 第一次疊代輸出0
print(next(g))
# 0
# 第二次疊代傳回值為1 運作yield後代碼輸出temp的值,因為沒有傳遞使用為None
print(next(g))
# 我是None,是通過send方法傳遞的參數
# 1
# 傳遞參數100,執行列印輸出temp的值
# send也是有傳回值的,為yield後的值,這裡沒有使用print輸出
g.send(100)
# 我是100,是通過send方法傳遞的參數
# 第四次疊代傳回值為3 運作yield後代碼輸出temp的值,因為沒有傳遞使用為None
print(next(g))
# 3
# 我是None,是通過send方法傳遞的參數
# 第五次疊代,執行到yield停止了,循環結束了,沒有執行到print這步
print(next(g))
# 4      

 

  對應的語句輸出如下

python協程系列(一)——生成器generator以及yield表達式詳解

   從上面可以看出yield語句與普通函數的return語句的差別在哪裡了,主要集中在以下幾點

  (1)return不能寫成“temp=return xxx”的形式,會提示文法錯誤,但是yield可以寫成"temp=yield xxx"的形式

  (2)普通函數return後面的語句都是不會再執行的,但是yield語句後面的依然會執行,但是需要注意的是,由于“延遲加載”特性,yield後面的代碼并不是在第一次疊代的時候執行的,而是在第二次疊代的時候才執行第一次yield後面沒有執行的代碼。也正是這個特性,構成了yield為什麼是實作協程最簡單的實作。

  個人備注:如果在生成器函數中定義return則遇到return則會報StopIteration錯誤立即退出生成器

  (3)使用send()方法傳進去的值,實際上就是yield表達式傳回的值,這就是為什麼前面每次輸出print(temp)都列印出None,因為沒有send值,是以temp為None,但是send(100)之後卻列印100,因為此時temp就是100了。

  個人備注:使用send()方法傳遞進去的值是指派給yield前面的變量temp了,yield的傳回值是yield後的值,這個傳回值也是send()方法的傳回值,即send()方法接收的傳回值為yield的傳回值

  我甚至還可以在yield後面不放任何東西,如下代碼:

# send()方法的使用
# yield不設定傳回值
def my_generator(n):
    for i in range(n):
        temp = yield 
        print('我是%s,是通過send方法傳遞的參數' %(temp))

g = my_generator(5)
print(next(g))
print(next(g))
g.send(100)
print(next(g))
print(next(g))
# None
# 我是None,是通過send方法傳遞的參數
# None
# 我是100,是通過send方法傳遞的參數
# 我是None,是通過send方法傳遞的參數
# None
# 我是None,是通過send方法傳遞的參數
# None      

  3,yield語句的用法總結

  yield的一般形式為:

  temp=yield 表達式(每次疊代要傳回的值)

  (1)如果要傳回确定的值,後面的表達式不可省略,絕大部分情況下我們也不省略,否則隻能傳回None

  (2)如果使用了send(value),傳遞進去的那個value會取代那個表達式的值,并且會将傳遞進去的那個值傳回給yield表達式的結果temp,是以如果想要在yield後面使用傳遞進去的那個值,必須要有使用temp,否則無法使用

  (3)yield語句的一般形式

temp = yield expression(推薦:既可以傳回疊代的值,也可以接受send進去的參數并使用)
yield expression(也可以使用:隻不過不能接受send傳遞的值)
temp = yield(不推薦 yield沒有設定傳回值)
yield(不推薦)      

  4,疊代器(生成器)的send()方法詳解

  主要目的是互動

  檢視send的定義,得到send(arg)是有傳回值的,而且他的傳回值就是原本我一個疊代處理的那個值,如下所示

# send的傳回值
def my_generator(n):
    for i in range(n):
        yield i

g=my_generator(5)

print(next(g))
print(next(g))
g.send(100)
print(next(g))
print(next(g))
# 0
# 1
# 3
# 4      

  我們發現雖然100傳進去了,但是他并沒有疊代出來,那原來的2去哪裡了呢?send(100)實際上就是傳回的2

  如果改為以下代碼擷取send的傳回值

# send傳回值2
def my_generator(n):
    for i in range(n):
        yield i

g=my_generator(5)

print(next(g))
print(next(g))
a = g.send(100)
print('我是send的傳回值%s' %(a))
print(next(g))
print(next(g))
# 0
# 1
# 我是send的傳回值2
# 3
# 4      

  send(arg)方法總結

  (1)它的主要作用是,機關需要手動更改生成器成某一個值并且使用它,則send發送進去一個暑假,然後報錯到yield語句的傳回值,以提供使用。

  (2)send(arg)的傳回值就是那個本來應該被疊代出來的那個值。這樣既可以保證我能傳入新的值,原來的值也不會弄丢。

  5,生成器的throw方法用法

  這個函數相比較于前面的next()、send()來說更加複雜,先看一下它的函數描述:

  raise exception in generator,return next yielded value or StopIteration,即在生成器中抛出異常,并且這個throw函數會傳回下一個要疊代的值或者是StopIteration。還是通過幾個例子來看吧!

# throw()
def my_generator():
    yield 'a'
    yield 'b'
    yield 'c'
g=my_generator()
print(next(g))
# a
print(next(g))
# b
print('-------------------------')
# 往生成器抛出異常
print(g.throw(StopIteration))
# StopIteration
# print(g.throw(TypeError))
# 生成器異常以後推出,以下語句不會執行
print(next(g))      

  輸出如下

python協程系列(一)——生成器generator以及yield表達式詳解

   因為在疊代完 b 之後,就觸發了StopIteration異常,這相當于後面的 ‘c’ 已經沒用了,跳過了c ,c再也不會執行,就中斷了,是以後面的 'c'再也不會疊代,是以這裡不會再傳回任何值,傳回的是StopIteration。

  再看一個例子

# throw()另外一個例子
def my_generator():
    try:
        yield 'a'
        yield 'b'
        yield 'c'
        yield 'd'
        yield 'e'
    except ValueError:
        print('觸發“ValueError"了')
    except TypeError:
        print('觸發“TypeError"了')

g=my_generator()
print(next(g))
print(next(g))
print('-------------------------')
print(g.throw(ValueError))
# 觸發異常,以下語句不執行
print('-------------------------')
print(next(g))
print(next(g))
print('-------------------------')
print(g.throw(TypeError))
print('-------------------------')
print(next(g))      
python協程系列(一)——生成器generator以及yield表達式詳解

   本例和上例差不多,都是往生成器抛入一個錯誤導緻生成器産生StopIteration錯誤,本例使用try...except...捕獲ValueError錯誤,接着生成器抛出StopIteration錯誤,整個生成器結束了,往後代碼不繼續執行了

  目前面兩次執行了a和b之後,向生成器扔進去一個異常,觸發ValueError異常,這時候意味着try後面的c、d、e已經廢棄了,不會再有用,這個生成器已經終止了,是以g.throw()會傳回StopIteration。

  再看一個例子:

# throw()再看一個例子
def my_generator():
    while True:
        try:
            yield 'a'
            yield 'b'
            yield 'c'
            yield 'd'
            yield 'e'
        except ValueError:
            print('觸發“ValueError"了')
        except TypeError:
            print('觸發“TypeError"了')

g=my_generator()
print(next(g))
# a
print(next(g))
# b
print('-------------------------')
# -------------------------
# 往生成器抛入ValueError錯誤首先執行捕獲錯誤列印 觸發“ValueError"了
# 因為使用while True無限循環,此時觸發錯誤後重新執行循環傳回下一個值,而不傳回StopIteration錯誤了
# 重新執行循環的下一個值是a是以列印傳回值a
print(g.throw(ValueError))
# 觸發“ValueError"了
# a
print('-------------------------')
# -------------------------
# 因為上一步以及執行到a了是以下面next方法繼續列印以後的yield值即b c
print(next(g))
# b
print(next(g))
# c
print('-------------------------')
# -------------------------
# 往生成器抛入TypeError錯誤首先執行捕獲錯誤列印 觸發“TypeError"了
# 因為使用while True無限循環,此時觸發錯誤後重新執行循環傳回下一個值,而不傳回StopIteration錯誤了
# 重新執行循環的下一個值是a是以列印傳回值a
print(g.throw(TypeError))
# 觸發“TypeError"了
# a
print('-------------------------')
# -------------------------
# 因為上一步以及執行到a了是以下面next方法繼續列印以後的yield值即b
print(next(g))
# b      

  解釋:

  出現這樣的結果是不是很意外?它和上面的那個例子隻有一個while之差,我們結果差那麼多,解釋如下:

  首先print(next(g))兩次:會輸出a,b,并停留在c之前.

  然後由于執行了g.throw(ValueError),是以會跳過所有後續的try語句,也就是說yield 'c'、yield 'd'、yield 'e'不會被執行,然後進入到except語句,列印出 觸發“ValueError"了。然後再次進入到while語句部分,消耗一個yield,此時因為是重新進入的while,消耗的依然是第一個yield 'a',是以會輸出a。實際上這裡的a也就是g.throw()的傳回值,因為它傳回的是下一個疊代的值;

  然後在print(next(g))兩次,會執行yield b’、yield 'c’語句,列印出b、c,并停留在執行完該語句後的位置,即yield 'd'之前。

  然後再g.throw(TypeError):會跳出try語句,進而後面的d,e不會被執行,下次自一次進入while,依然列印出a。

  最後,執行了一次print(next(g)),列印出b。

python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解

   說明:為什麼加while可以使方法throw傳回下一個yield的值,而不是抛出StopIteration錯誤,因為while True無限循環,當使用throw往生成器抛入錯誤時,因為while内部使用是try捕獲錯誤,是以即使發生錯誤也不會直接抛出錯誤而是根據錯誤類型執行對應的except,執行完except以後繼續執行while無限循環繼續執行生成器。像上一個例子沒有使用while是可以捕獲到對應的錯誤,因為沒有while語句是以沒有無限循環則在抛入錯誤以後繼續往下執行但是因為繼續往下執行沒有yield了是以抛出StopIteration錯誤。

  下面使用調試模式分析執行過程

python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解
python協程系列(一)——生成器generator以及yield表達式詳解

   注意:本例手動往生成器抛入幾個錯誤觀察throw()方法的傳回,throw()方法傳回下一個值或者StopIteration。

      如果不手動往生成器抛入錯誤而是一直執行next()方法則本生成器是一個無限的生成器,生成器的内容為a b c d e重複無限輸出

  6,生成器的啟動與關閉close

  (1)生成器的啟動

  使用close()方法手動關閉生成器函數,後面的調用會直接傳回StopIteration異常

  這裡所讨論的啟動不是使用for循環疊代,我們在使用for循環疊代的時候可能沒有去考慮“啟動”與“關閉”這些事情,這裡指的是使用next()内置方法一個一個疊代的情形。在第一次疊代的時候,一定要先啟動生成器,啟動的兩種方法為:

  第一:直接使用next(g),這會直接開始疊代第一個元素(推薦使用這個啟動)

  第二:使用g.send(None)進行啟動,注意第一次啟動隻能傳入None。如果傳入其他具體的值則會報錯。

def my_generator():
    yield 1
    yield 2
    yield 3
    yield 4

g = my_generator()
# 第一次啟動,本來第一次應該疊代的1,這裡被取代了,但是send(None)會傳回1
# g.send(None)等價于next(g)
g.send(None)  
print(next(g))
# 2
print(next(g))
# 3
print(next(g))
# 4      

  (2)生成器關閉通過close()方法

  如果一個生成器被中途關閉之後,在此調用next()方法,則會顯示錯誤,如下:

# 關閉生成器close()方法

def my_generator():
    yield 1
    yield 2
    yield 3
    yield 4

g = my_generator()
print(next(g))
# 1
print(next(g))
# 2
g.close()
# #在此處會顯示錯誤
print(next(g))      

  7,生成器終止疊代-StopIteration

  前面講的手動關閉生成器,使用close()方法,後面的疊代或抛出StopIteration異常。另外

  在一個生成器中,如果沒有return,則預設執行到函數完畢時傳回StopIteration;

def g1():
    yield 1

g = g1()
# 第一次調用next(g)時,會在執行完yield語句後挂起,使用此時程式并沒有執行結束
next(g)
# 第二次執行next(g)時,程式試圖從yield的下一條語句開始執行,發現已經到了結尾,是以抛出StopIteration異常
next(g)      

  如果遇到return,如果在執行過程中return,則直接抛出StopIteration終止疊代

# 遇到return立即抛出StopIteration異常
def g2():
    yield 'a'
    return
    yield 'b'

g = g2()
# 執行完以下語句程式停留在yield 'a'語句後的位置
next(g)
# 程式發現下一條語句是return是以抛出StopIteration異常,這樣yield 'b'語句永遠也不會執行
next(g)
# StopIteration      

  如果在return後傳回一個值,那麼這個值為StopIteration異常的說明,不是程式的傳回值

# return如果有傳回值則是異常StopIteration的說明而不是函數的傳回值
def g3():
    yield 'a'
    return '這是錯誤說明'
    yield 'b'

g = g3()
next(g)
next(g)      

  運作結果為

python協程系列(一)——生成器generator以及yield表達式詳解

   注意:生成沒有辦法使用return來傳回值。因為return傳回的那個值是通過StopIteration的異常資訊傳回的,是以沒有辦法直接擷取這個return傳回的值。

  當然上面說的無法擷取return傳回值,我們指的是沒有辦法通過result=g3()這種形式擷取return的傳回值。實際上還是有手段擷取這個return的值,有兩種方法

  方法一,使用後面的yield from語句(下文再講解)

  方法二,因為return傳回的值是作為StopIteration的一個value屬性存在的,StopIteration本質是是一個類,是以可以通過通路它的value屬性擷取這個return傳回的值,使用以下代碼

# 擷取return的傳回值
def g3():
    yield 'a'
    return '這是錯誤說明'
    yield 'b'

g = g3()
try:
    # 疊代a
    next(g)
    # 觸發異常
    next(g)
except StopIteration as exc:
    result = exc.value
    print(result)

# 這是錯誤說明      

  

下一篇: 曾記否