標籤:
原創作品,轉載請註明出處:點我
上一篇文章Python進階編程之產生器(Generator)與coroutine(一):Generator中,我們介紹了什麼是Generator,以及寫了幾個使用Generator Function的樣本,這一小節,我們會介紹Python的coroutine,以及會有一個小例子,再接下來的文章中會以代碼的形式一步步介紹coroutine的進階用法。
coroutine(協程)
什麼是coroutine?coroutine跟Generator有什麼區別?下面先看一段代碼:
1 def grep_co(pattern):2 print "Lookin for %s" % pattern3 while True:4 # 執行完下面這句,函數掛起,等待接收資料,通過send()函數5 # yield line6 line = (yield )7 if pattern in line:8 print line
grep_co就是一個coroutine,從代碼的角度來看,coroutine跟generator的唯一區別就是在第6行,coroutine是
line = (yield)
而Generator是:
yield line
那麼這兩者有什麼區別呢?先別急,我們接著往下看:
在Python2.5及之後的版本中,yield可以用作為運算式,就是gerp_co()函數中的這種用法。那麼問題來了,gerp_co()函數中的line的值應該是多少?這個值是誰給他的?
答案很簡單:line的值是我們(grep_co())的調用者發送過去的。怎麼發送的?Easy!使用send(value)函數即可。先來看下執行效果:
1 >>> def grep_co(pattern): 2 print "Looking for %s" %pattern 3 while True: 4 line = (yield) 5 if pattern in line: 6 print line 7 8 9 >>> g = grep_co("python")10 >>> g11 <generator object grep_co at 0x01FA3B98>
跟Generator一樣,當調用gerp_co("python")的時候,並不會立即執行grep_co函數,而是會返回一個generator類型的對象,同樣是在這個對象調用了next()的時候才會開始執行這個函數:
>>> g.next()Looking for python
調用了next()函數之後,函數開始執行,執行到第6行,也就是line = (yield)這一行代碼的時候,碰到了yield關鍵字,跟Generator一樣,此時,整個函數會掛起,由於yield後面沒有跟隨其他的變數(一般情況下,coroutine中的yield語句也不會跟隨傳回值,這個後面會講到),所以此時不會返回任何資料,只是單純的儲存執行環境並掛起暫停執行。
既然coroutine跟Generator一樣碰到yield關鍵字會掛起,那麼是不是也跟Generator一樣調用next()函數繼續執行呢?其實你如果能夠這樣想我會很高興,說明你有在認真的看,O(∩_∩)O~。不幸的是想法是好的,可惜是錯的,O(∩_∩)O哈哈~,應該使用send(value)函數。接著上面往下走,上面調用了g.next(),函數開始執行,碰到了yield關鍵字,函數掛起暫停執行,現在調用g.send("I love python")函數,執行結果如下:
>>> g.send("Hello,I love python")Hello,I love python
可以看到,send 函數有一個參數,這個參數就是傳遞個line這個變數的。調用了send("I love python")這個函數之後,grep_co()這個函數會接著上次掛起的地方往下執行,也就是在第六行line = (yield)這個地方,send("I love python")函數的參數會被傳遞給line這個變數,然後接著往下執行,直到執行完畢或者再次碰到yield關鍵字。在這個例子中,line的值是"I love pyhton",pattern的值是"python",if判斷為真,列印輸出line,接著往下執行,因為是在一個無限迴圈當中,再次碰到了yield這個關鍵字,掛起並暫停。所以我們會看到上面的執行結果。
>>> g.send("Life is short,Please use Python")>>> g.send("Life is short,Please use python")Life is short,Please use python
我們再繼續調用send(value)函數,會重複上面的執行過程。
講了這麼多,那麼什麼才是coroutine呢?我相信聰明的你應該已經猜到了:
所謂的coroutine,也就是一個包含有yield關鍵字的函數,但是跟Generator不同的是,coroutine會以value = (yield)的方式使用yield關鍵字,並且接受調用者通過send(value)函數發送過來的資料,然後消費這個資料(consume the value)。
在使用coroutine,有一點很需要注意的就是:所有的coroutine必須要先調用.next()或者send(None)才行。在調用send傳入非None值前,產生器必須處於掛起狀態,否則將拋出異常。當然,也可以使用.next()恢複產生器,只不過此時coroutine接收到的value為None。
可以調用.close()關閉coroutine。關閉coroutine之後,再次調用.nect()或者.send(value)之後會拋出異常。
>>> g.close()>>> g.send("corotuine has already closed")Traceback (most recent call last): File "<pyshell#16>", line 1, in <module> g.send("corotuine has already closed")StopIteration>>>
.close()會拋出GeneratorExit異常,我們可以在代碼中捕獲並處理這個異常,而且一般情況下也應該處理這個異常。
1 def grep(pattern):2 print "Looking for %s" %pattern3 try:4 while True:5 line = (yield)6 if pattern in line:7 print line8 except GeneratorExit:9 print "Going away.Goodbye"
當然也可以通過throw()函數在產生器內部拋出一個指定的異常。
>>> g.send("Life is short,please use python")Life is short,please use python>>> g.throw(RuntimeError,"You‘ar hosed")Traceback (most recent call last): File "<pyshell#14>", line 1, in <module> g.throw(RuntimeError,"You‘ar hosed") File "<pyshell#10>", line 5, in grep line = (yield)RuntimeError: You‘ar hosed
好了,coroutine已經介紹的差不多了,我們可以看到coroutine可以很方便的掛起和執行,也有多個人口點和出口點,而普通的函數一般只有一個進入點和出口點。
Generator和coroutine用起來很像,但是僅此而已,Generator和coroutine是兩個完全不相同的概念。Generator產生(返回)資料用來在迭代(iterator)中使用,而coroutine則是需要其他的地方發送資料過來,從而消費資料(consume value)。
接下來講的是使用coroutine要注意的地方:
第一,就是千萬別忘記了在使用coroutine前要先調用.next()函數。但是這一點經常容易忘記,所以可以使用一個function decorator.
1 # 作為裝飾器用,因為經常容易會忘記調用.next()函數2 def coroutine(func):3 def start(*args,**kargs):4 cr = func(*args,**kargs)5 cr.next()6 return cr7 return start
第二:最好是不要把Generator和coroutine混合在一起用,也就是receive_value = (yield return_value)這種方式來用。因為這會很難以理解,而且也會出現某些很詭異的情況。先看代碼:
1 def countdown_co(n):2 print "Counting down from ",n3 while n >= 0:4 newvalue = (yield n)5 # 如果接收到了newvalue,則重新設定n6 if newvalue is not None:7 n = newvalue8 else:9 n -= 1
代碼很簡單,同時使用了coroutine和Generator,詭異的情況發生了。先是寫一個函數test_countdown_co():
1 def test_countdown_co():2 c = countdown_co(5)3 for n in c:4 print n5 if 5 == n:6 c.send(3)
然後在IDLE終端調用這個函數,可以看到函數的執行結果為:
>>> test_countdown_co()Counting down from 55210
現在,我們在IDLE終端直接輸入上面的test_countdown_co()代碼來測試countdown_co()函數:
>>> c = countdown_co(5)>>> for n in c: print n if 5 == n: c.send(3) Counting down from 553210>>>
可以看到一樣的代碼,執行結果卻不一樣,好詭異啊!到現在我都沒有想明白這是為什麼。如果有誰知道原因,請告訴我,O(∩_∩)O謝謝!
好了!這一篇介紹coroutine的Blog也寫好了。接下來的文章會以完整的代碼的形式來介紹coroutine的一些進階用法。敬請期待。O(∩_∩)O哈哈~
Python進階編程之產生器(Generator)與coroutine(二):coroutine介紹