原創作品,轉載請注明出處:點我
上一篇文章Python高級編程之生成器(Generator)與coroutine(一):Generator中,我們介紹了什么是Generator,以及寫了幾個使用Generator Function的示例,這一小節,我們會介紹Python的coroutine,以及會有一個小例子,再接下來的文章中會以代碼的形式一步步介紹coroutine的高級用法。
coroutine(協程)
什么是coroutine?coroutine跟Generator有什么區別?下面先看一段代碼:
1 def grep_co(pattern): 2 print "Lookin for %s" % pattern 3 while True: 4 # 執行完下面這句,函數掛起,等待接收數據,通過send()函數 5 # yield line 6 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 >>> g 11 <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" %pattern 3 try: 4 while True: 5 line = (yield) 6 if pattern in line: 7 print line 8 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 cr 7 return start
第二:最好是不要把Generator和coroutine混合在一起用,也就是receive_value = (yield return_value)這種方式來用。因為這會很難以理解,而且也會出現某些很詭異的情況。先看代碼:
1 def countdown_co(n): 2 print "Counting down from ",n 3 while n >= 0: 4 newvalue = (yield n) 5 # 如果接收到了newvalue,則重新設置n 6 if newvalue is not None: 7 n = newvalue 8 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 n 5 if 5 == n: 6 c.send(3)
然后在IDLE終端調用這個函數,可以看到函數的執行結果為:
>>> test_countdown_co() Counting down from 5 5 2 1 0
現在,我們在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 5 5 3 2 1 0 >>>
可以看到一樣的代碼,執行結果卻不一樣,好詭異啊!到現在我都沒有想明白這是為什么。如果有誰知道原因,請告訴我,O(∩_∩)O謝謝!
好了!這一篇介紹coroutine的Blog也寫好了。接下來的文章會以完整的代碼的形式來介紹coroutine的一些高級用法。敬請期待。O(∩_∩)O哈哈~