談談 Python 的生成器

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

defcount(n):

x=0

whilex

yieldx

x+=1

foriincount(5):

printi

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

classCountIter:

def__init__(self,n):

self.n=n

def__iter__(self):

self.x= -1

returnself

defnext(self): # For Python 2.x

self.x+=1

ifself.x

returnself.x

else:

raiseStopIteration

foriinCountIter(5):

printi

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

gen=count(2)

printgen.next()# 0

printgen.next()# 1

printgen.next()# StopIteration

談談 Python 的生成器

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

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

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

gen=(xforxinrange(5))# 注意這裡是(),而不是[]

foriingen:

printi

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

1. close()方法

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

gen=(xforxinrange(5))

gen.close()

gen.next()# StopIteration

2. send()方法

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

defcount(n):

x=0

whilex

value=yieldx

ifvalueisnotNone:

print'Received value: %s'%value

x+=1

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

gen=count(5)

printgen.next()# print 0

printgen.send('Hello')# Received value: Hello, then print 1

談談 Python 的生成器

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

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

3. throw()方法

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

defthrow_gen():

try:

yield'Normal'

exceptValueError:

yield'Error'

finally:

print'Finally'

gen=throw_gen()

printgen.next()# Normal

printgen.next()# Finally, then StopIteration

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

gen=throw_gen()

printgen.next()# Normal

printgen.throw(ValueError)# Error

printgen.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()喚醒。相比多線程,協程的好處是它在一個線程內執行,避免線程之間切換帶來的額外開銷,而且多線程中使用共享資源,往往需要加鎖,而協程不需要,因為代碼的執行順序是你完全可以預見的,不存在多個線程同時寫某個共享變量而導致出錯的情況。

談談 Python 的生成器

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

defconsumer():

last=''

whileTrue:

receival=yieldlast

ifreceivalisnotNone:

print'Consume %s'%receival

last=receival

defproducer(gen,n):

gen.next()

x=0

whilex

x+=1

print'Produce %s'%x

last=gen.send(x)

gen.close()

gen=consumer()

producer(gen,5)

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


分享到:


相關文章: