當前位置:
首頁 > 知識 > 談談 Python 的生成器

談談 Python 的生成器

點擊上方「

Python開發

」,選擇「置頂公眾號」


關鍵時刻,第一時間送達!






第一次看到Python代碼中出現yield關鍵字時,一臉懵逼,完全理解不了這個。網上查下解釋,函數中出現了yield關鍵字,則調用該函數時會返回一個生成器。那到底什麼是生成器呢?我們經常看到類似下面的代碼




def

count

(

n

)

:

    

x

=

0


    

while

x

<

n

:


        

yield

x


        

x

+=

1



for

i

in

count

(

5

)

:


    

print

i




這段代碼執行後列印序列0到4,所以我一開始以為這個生成器就是生成一個序列呀。那這跟迭代器有什麼區別呢?我們來看下迭代器的例子:




class

CountIter

:


    

def

__init__

(

self

,

n

)

:


        

self

.

n

=

n



    

def

__iter__

(

self

)

:


        

self

.

x

= -

1


        

return

self



    

def

next

(

self

)

:  

# For Python 2.x


        

self

.

x

+=

1


        

if

self

.

x

<

self

.

n

:


            

return

self

.

x


        

else

:


            

raise

StopIteration



for

i

in

CountIter

(

5

)

:


    

print

i




CountIter類就是一個迭代器,它的__iter__()方法返回可迭代對象,next()方法則執行下一輪迭代(註:在Python 3.x里是__next__()方法)。上面的代碼執行後也會列印序列0到4,看上去跟之前的生成器效果一樣,就是代碼長一點。不僅如此,生成器自帶next()方法,而且在越界時也會拋出StopIteration異常。





gen

=

count

(

2

)


print

gen

.

next

()

# 0


print

gen

.

next

()

# 1


print

gen

.

next

()

# StopIteration




那區別到底是什麼,在何種情況下,我們應該使用生成器呢?




每次執行迭代器的next()方法並返回後,該方法的上下文環境即消失了,也就是所有在next()方法中定義的局部變數就無法被訪問了。而對於生成器,每次執行next()方法後,代碼會執行到yield關鍵字處,並將yield後的參數值返回,同時當前生成器函數的上下文會被保留下來。也就是函數內所有變數的狀態會被保留,同時函數代碼執行到的位置會被保留,感覺就像函數被暫停了一樣。當再一次調用next()方法時,代碼會從yield關鍵字的下一行開始執行。很神奇吧!如果執行next()時沒有遇到yield關鍵字即退出(或返回),則拋出StopIteration異常。




本文的第一個例子是使用生成器函數來構造生成器,Python也提供了生成器表達式,下面的例子也可以列印序列0到4。





gen

=

(

x

for

x

in

range

(

5

))

  

# 注意這裡是(),而不是[]


for

i

in

gen

:


    

print

i




到目前為止,我們了解了生成器同迭代器在實現機制上的不同,但似乎功能是一樣的,那生成器的存在有什麼價值呢?我們先來看看除了next()方法外,生成器還提供了哪些方法。




1. close()方法




顧名思義,close()方法就是關閉生成器。生成器被關閉後,再次調用next()方法,不管能否遇到yield關鍵字,都會立即拋出StopIteration異常。





gen

=

(

x

for

x

in

range

(

5

))


gen

.

close

()


gen

.

next

()

  

# StopIteration




2. send()方法




這是我認為生成器最重要的功能,我們可以通過send()方法,向生成器內部傳遞參數。我們來看個例子:





def

count

(

n

)

:


    

x

=

0


    

while

x

<

n

:


        

value

=

yield

x


        

if

value

is

not

None

:


            

print

"Received value: %s"

%

value


        

x

+=

1




還是之前的count函數,唯一的區別是我們將」yield x」的值賦給了變數value,並將其列印出來。如何給value傳值呢?





gen

=

count

(

5

)


print

gen

.

next

()

  

# print 0


print

gen

.

send

(

"Hello"

)

  

# Received value: Hello, then print 1




我們先調用next()方法,讓代碼執行到yield關鍵字(這步必須要),當前列印出0。然後當我們調用」gen.send(『Hello』)」時,字元串』Hello』就被傳入生成器中,並作為yield關鍵字的執行結果賦給變數」value」,所以控制台會列印出」Received value: Hello」。然後代碼繼續執行,直到下一次遇到yield關鍵字後暫定,此時生成器返回的是1。




簡單的說,send()就是next()的功能,加上傳值給yield。如果你有興趣看下Python的源碼,你會發現,其實next()的實現,就是send(None)。




3. throw()方法




除了向生成器函數內部傳遞參數,我們還可以傳遞異常。還是先看例子:





def

throw_gen

()

:


    

try

:


        

yield

"Normal"


    

except

ValueError

:


        

yield

"Error"


    

finally

:


        

print

"Finally"



gen

=

throw_gen

()


print

gen

.

next

()

  

# Normal


print

gen

.

next

()

  

# Finally, then StopIteration




如果像往常一樣調用next()方法,會返回』Normal』。再次調用next(),會進入finally語句,列印』Finally』,同時由於函數退出,生成器會拋出StopIteration異常。我們換個方式,在第一次調用next()方法後,調用throw()方法,情況會怎樣?





gen

=

throw_gen

()


print

gen

.

next

()

  

# Normal


print

gen

.

throw

(

ValueError

)

    

# Error


print

gen

.

next

()

  

# Finally, then StopIteration




我們會看到,throw()方法向生成器函數內部傳遞了」ValueError」異常,代碼進入」except ValueError」語句,當遇到下一個yield時才暫停並退出,此時生成器返回的是』Error』字元串。簡單的說,throw()就是next()的功能,加上傳異常給yield。




聊到這裡,相信大家對生成器的功能已經有了一個很好的理解。生成器不但可以逐步生成序列,不用像列表一樣初始化時就要開闢所有的空間。它更大的價值,我個人認為,就是模擬並發。很多朋友可能已經知道,Python雖然可以支持多線程,但由於GIL(全局解釋鎖,Global Interpreter Lock)的存在,同一個時間,只能有一個線程在運行,所以無法實現真正的並發。我們暫且不討論GIL存在的意義,這裡我們提出了一個新的概念,就是協程(Coroutine)。




Python實現協程最簡單的方法,就是使用yield。當一個函數在執行過程中被阻塞時,就用yield掛起,然後執行另一個函數。當阻塞結束後,可以用next()或者send()喚醒。相比多線程,協程的好處是它在一個線程內執行,避免線程之間切換帶來的額外開銷,而且多線程中使用共享資源,往往需要加鎖,而協程不需要,因為代碼的執行順序是你完全可以預見的,不存在多個線程同時寫某個共享變數而導致出錯的情況。




我們來使用協程寫一個生產者消費者的例子:





def

consumer

()

:


    

last

=

""


    

while

True

:


        

receival

=

yield

last


        

if

receival

is

not

None

:


            

print

"Consume %s"

%

receival


            

last

=

receival



def

producer

(

gen

,

n

)

:


    

gen

.

next

()


    

x

=

0


    

while

x

<

n

:


        

x

+=

1


        

print

"Produce %s"

%

x


        

last

=

gen

.

send

(

x

)



    

gen

.

close

()



gen

=

consumer

()


producer

(

gen

,

5

)




執行下例子,你會看到控制台交替列印出生產和消費的結果。消費者consumer()函數是一個生成器函數,每次執行到yield時即掛起,並返回上一次的結果給生產者。生產者producer()接收到生成器的返回,並生成一個新的值,通過send()方法發送給消費者。至此,我們成功實現了一個(偽)並發。




本文中的示例代碼可以在這裡下載(http://python.jobbole.com/downloads/201608/python-yield.tar.gz)。






  • 來源:思誠之道




  • www.bjhee.com/python-yield.html



  • Python開發整理髮布,轉載請聯繫作者獲得授權


【點擊成為Java大神】

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發 的精彩文章:

面試題:布爾變數

TAG:Python開發 |