python生成器(轉)
生成器是一種特殊的叠代器,內部支持了生成器協議,不需要明確定義__iter__()和next()方法。生成器通過生成器函數產生,生成器函數可以通過常規的def語句來定義,但是不用return返回,而是用yield一次返回一個結果。
一、yield和叠代器生成器
叠代器是非常高效的類型,無論是從時間復雜度,還是從空間復雜度。而實現叠代器的代碼雖然簡單,卻也繁瑣。為此,python定義了一個yield關鍵字,專門用來構造叠代器。yield有生成,產生的意思。
yield的功能和return非常類似,它們都只能在方法中使用。不同的是,包含yield語句的方法被稱為生成器方法。當調用生成器方法時,會返回一個生成器對象。
例如,看下面的例子。
def MyGenerator(): yield 1 gen = MyGenerator() print gen
輸出結果為
<generator object MyGenerator at 0x0000000001D9DD80>
當調用生成器對象的next方法時,會執行生成器方法中的代碼,直至遇到yield語句時,方法的執行過程會被掛起。同時,方法運行的上下文環境會被保存。而next方法的返回值就是yield關鍵字後面表達式的返回值。
例如,下面代碼
print gen.next()
執行結果為
1
當我們繼續調用next方法時,從上一次掛起的地方開始,繼續執行後面的代碼。直至遇到下一個yield語句。當方法執行完畢,依然沒有遇到yield語句,拋出StopIteration異常。
例如
def MyGenerator(): yield 1 yield ‘a‘ gen = MyGenerator() print gen.next() print gen.next() print gen.next()
上面代碼中第1次調用next方法,執行語句yield 1。第2次調用next方法,執行語句yield ‘a‘。第3次調用next方法時,在方法退出前都沒有遇到yield語句,因此拋出StopIteration異常。
上面介紹的生成器方法的工作機理。在後面的博文中,會逐步介紹生成器方法的一些經典應用。
二、通過生成器函數構造序列對象的叠代器
事實上,一個序列對象的叠代器,依賴於一個整數序列的叠代器。看下面的代碼
def MyGenerator(len): start = 0 while start < len: yield start start = start + 1 gen = MyGenerator(3) print gen.next() print gen.next() print gen.next() print gen.next()
當調用第1次next方法時, 會首先執行MyGenerator方法的第1行代碼start = 0。然後進入循環。這裏len的值通過參數傳入為3。因此while的條件表達式為真。進入循環後,遇到yield語句,方法的執行過程被掛起。next方法的返回值為start的值,即0。
當調用第2次next方法時,接著上面的掛起點,往下執行start = start + 1語句,start的值變為1。接著又進入while循環的條件判斷,start<len依然為真。因此,又執行yield語句。但是由於start值為1,故而這一次next方法返回的值為1。
第3次next方法的調用類似。
當調用第4次next方法時,while循環的條件判斷start < len為假,while循環結束,MyGenerator方法調用也隨之結束,拋出StopIteration異常。
輸出結果
0 1 2 Traceback (most recent call last): File "test.py", line 21, in <module> print gen.next() StopIteration
有了上面的結果,重寫序列對象的叠代器輕而易舉。
def MyGenerator(sequence): start = 0 while start < len(sequence): yield sequence[start] start = start + 1 gen = MyGenerator([1,2,3,‘a‘,‘b‘,‘c‘]) for i in gen: print i
對比之前叠代器類的代碼,我們可以認識到,yield關鍵字為構造叠代器提供了多大的方便。它使得代碼長度縮減許多,同時也大大增強了可讀性。
三、生成器對象的send方法
生成器對象是一個叠代器。但是它比叠代器對象多了一些方法,它們包括send方法,throw方法和close方法。這些方法,主要是用於外部與生成器對象的交互。本文先介紹send方法。
send方法有一個參數,該參數指定的是上一次被掛起的yield語句的返回值。這樣說起來比較抽象,看下面的例子。
def MyGenerator(): value = (yield 1) value = (yield value) gen = MyGenerator() print gen.next() print gen.send(2) print gen.send(3)
輸出的結果如下
1 2 Traceback (most recent call last): File "test.py", line 18, in <module> print gen.send(3) StopIteration
上面代碼的運行過程如下。
當調用gen.next()方法時,Python首先會執行MyGenerator方法的yield 1語句。由於是一個yield語句,因此方法的執行過程被掛起,而next方法返回值為yield關鍵字後面表達式的值,即為1。
當調用gen.send(2)方法時,python首先恢復MyGenerator方法的運行環境。同時,將表達式(yield 1)的返回值定義為send方法參數的值,即為2。這樣,接下來value=(yield 1)這一賦值語句會將value的值置為2。繼續運行會遇到yield value語句。因此,MyGenerator方法再次被掛起。同時,send方法的返回值為yield關鍵字後面表達式的值,也即value的值,為2。
當調用send(3)方法時MyGenerator方法的運行環境。同時,將表達式(yield value)的返回值定義為send方法參數的值,即為3。這樣,接下來value=(yield value)這一賦值語句會將value的值置為3。繼續運行,MyGenerator方法執行完畢,故而拋出StopIteration異常。
總的來說,send方法和next方法唯一的區別是在執行send方法會首先把上一次掛起的yield語句的返回值通過參數設定,從而實現與生成器方法的交互。但是需要註意,在一個生成器對象沒有執行next方法之前,由於沒有yield語句被掛起,所以執行send方法會報錯。例如
gen = MyGenerator() print gen.send(2)
上面代碼的輸出為
Traceback (most recent call last): File "test.py", line 16, in <module> print gen.send(2) TypeError: can‘t send non-None value to a just-started generator
當然,下面的代碼是可以接受的
gen = MyGenerator() print gen.send(None)
因為當send方法的參數為None時,它與next方法完全等價。但是註意,雖然上面的代碼可以接受,但是不規範。所以,在調用send方法之前,還是先調用一次next方法為好。
四、生成器對象的throw方法
上邊介紹的send方法,通過向生成器對象傳遞參數來實現與生成器對象的交互。本文介紹與生成器對象的另一種方式,即throw方法。它的實現手段是通過向生成器對象在上次被掛起處,拋出一個異常。之後會繼續執行生成器對象中後面的語句,直至遇到下一個yield語句返回。如果在生成器對象方法執行完畢後,依然沒有遇到yield語句,拋出StopIteration異常。
請看下面的例子
def myGenerator(): value = 1 while True: yield value value += 1 gen = myGenerator() print gen.next() print gen.next() print gen.throw(Exception, "Method throw called!")
輸出結果為
1 2 Traceback (most recent call last): File "test.txt", line 11, in <module> print gen.throw(Exception, "Method throw called!") File "test.txt", line 4, in myGenerator yield value Exception: Method throw called!
代碼的最後一句向生成器對象拋出了一個異常。但是,在生成器對象的方法時沒有處理該異常的代碼,因此異常會被拋出到主方法。
下面的示例中,添加了處理異常的代碼
def myGenerator(): value = 1 while True: try: yield value value += 1 except: value = 1 gen = myGenerator() print gen.next() print gen.next() print gen.throw(Exception, "Method throw called!")
在上面的代碼中,加入了一個try-except語句塊處理異常。當生成器方法收到異常後,會調到except語句塊,將value置為1。因此,代碼的輸出如下。
1 2 1 Exception RuntimeError: ‘generator ignored GeneratorExit‘ in <generator object myGenerator at 0x00000000028BB900> ignored
上面輸出中,第2個1是gen.throw方法的返回值。在執行完該方法後,生成器對象方法的while循環並沒有結束,也即是說生成器方法的執行還沒有結束。這個時候如果強制結束主程序,會拋出一個RuntimeError。也就是上面輸出的第4行。要優雅地關閉主程序,需要用到生成器對象的close方法。
五、GeneratorExit異常
當一個生成器對象被銷毀時,會拋出一個GeneratorExit異常。請看下面的代碼。
def myGenerator(): try: yield 1 except GeneratorExit: print "myGenerator exited" gen = myGenerator() print gen.next()
輸出結果為
1
myGenerator exited
上面代碼的運行邏輯如下: 當調用到gen.next()方法時,會執行生成器對象方法的yield語句。此後,主程序結束,系統會自動產生一個GeneratorExit異常,被生成器對象方法的Except語句塊截獲。
而GeneratorExit異常產生的時機,是在生成器對象被銷毀之前。為了驗證這點,請看下面的代碼。
def myGenerator(): try: yield 1 yield 2 except GeneratorExit: print "myGenerator exited" gen = myGenerator() print gen.next() del gen print "Main caller exited"
輸出結果
1
myGenerator exited
Main caller exited
值得一提的是,GeneratorExit異常只有在生成器對象被激活後,才有可能產生。更確切的說,需要至少調用一次生成器對象的next方法後,系統才會產生GeneratorExit異常。請看下面的代碼。
def myGenerator(): try: yield 1 yield 2 except GeneratorExit: print "myGenerator exited" gen = myGenerator() del gen print "Main caller exited"
其輸出結果如下:
Main caller exited
在上面的示例中,我們都顯式地捕獲了GeneratorExit異常。如果該異常沒有被顯式捕獲,生成器對象也不會把該異常向主程序拋出。因為GeneratorExit異常定義的初衷,是方便開發者在生成器對象調用結束後定義一些收尾的工作,如釋放資源等。
六、生成器對象的close方法
生成器對象的close方法會在生成器對象方法的掛起處拋出一個GeneratorExit異常。GeneratorExit異常產生後,系統會繼續把生成器對象方法後續的代碼執行完畢。參見下面的代碼。
def myGenerator(): try: yield 1 print "Statement after yield" except GeneratorExit: print "Generator error caught" print "End of myGenerator" gen = myGenerator() print gen.next() gen.close() print "End of main caller"
代碼執行過程如下:
- 當調用gen.next方法時,會激活生成器,直至遇到生成器方法的yield語句,返回值1。同時,生成器方法的執行被掛起。
- 當調用gen,close方法時,恢復生成器方法的執行過程。系統在yield語句處拋出GeneratorExit異常,執行過程跳到except語句塊。當except語句塊處理完畢後,系統會繼續往下執行,直至生成器方法執行結束。
代碼的輸出如下:
1
Generator error caught
End of myGenerator
End of main caller
需要註意的是,GeneratorExit異常的產生意味著生成器對象的生命周期已經結束。因此,一旦產生了GeneratorExit異常,生成器方法後續執行的語句中,不能再有yield語句,否則會產生RuntimeError。請看下面的例子。
def myGenerator(): try: yield 1 print "Statement after yield" except GeneratorExit: print "Generator error caught" yield 3 gen = myGenerator() print gen.next() gen.close() print "End of main caller"
輸出結果為
1 Generator error caught Traceback (most recent call last): File "test.txt", line 12, in <module> gen.close() RuntimeError: generator ignored GeneratorExit
註意,由於RuntimError會向主方法拋出,因此主方法最後的print語句沒有執行。
有了上面的知識,我們就可以理解為什麽下面的代碼會拋出RuntimError錯誤了。
def myGenerator(): value = 1 while True: try: yield value value += 1 except: value = 1 gen = myGenerator() print gen.next() print gen.next() print gen.throw(Exception, "Method throw called!")
上面代碼中,當主程序結束前,系統產生GeneratorExit異常,被生成器對象方法的except語句捕獲,但是此時while語句還沒有退出,因此後面還會執行“yield value”這一語句,從而發生RuntimeError。要避免這個錯誤非常簡單,請看下面的代碼。
def myGenerator(): value = 1 while True: try: yield value value += 1 except Exception: value = 1 gen = myGenerator() print gen.next() print gen.next() print gen.throw(Exception, "Method throw called!")
代碼第7行的except語句聲明只捕獲Exception異常對象。這樣,當系統產生GeneratorExit異常後,不再被except語句捕獲,繼續向外拋出,從而跳出了生成器對象方法的while語句。
這裏再簡單說一句,GeneratorExit異常繼承自BaseException類。BaseException類與Exception類不同。一般情況下,BaseException類是所有內建異常類的基類,而Exception類是所有用戶定義的異常類的基類。
轉自:http://blog.csdn.net/hedan2013/article/details/72811117
python生成器(轉)