本文主要介紹python中Enhanced generator即coroutine相關內容,包括基本語法、使用場景、注意事項,以及與其他語言協程實現的異同。
enhanced generator
在上文介紹了yield和generator的使用場景和主意事項,只用到了generator的next方法,事實上generator還有更強大的功能。PEP 342為generator增加了一系列方法來使得generator更像一個協程Coroutine。做主要的變化在於早期的yield只能返回值(作為資料的產生者), 而新增加的send方法能在generator恢復的時候消費一個數值,而去caller(generator的呼叫著)也可以通過throw在generator掛起的主動丟擲異常。
首先看看增強版本的yield,語法格式如下:
1 |
back_data = yield cur_ret |
這段程式碼的意思是:當執行到這條語句時,返回cur_ret給呼叫者;並且當generator通過next()或者send(some_data)方法恢復的時候,將some_data賦值給back_data.例如:
1 2 3 4 5 6 7 8 9 10 11 12 |
def gen(data): print 'before yield', data back_data = yield data print 'after resume', back_data if __name__ == '__main__': g = gen(1) print g.next() try: g.send(0) except StopIteration: pass |
輸出:
before yield 1
1
after resume 0
兩點需要注意:
(1) next() 等價於 send(None)
(2) 第一次呼叫時,需要使用next()語句或是send(None),不能使用send傳送一個非None的值,否則會出錯的,因為沒有Python yield語句來接收這個值。
應用場景
當generator可以接受資料(在從掛起狀態恢復的時候) 而不僅僅是返回資料時, generator就有了消費資料(push)的能力。下面的例子來自這裡:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
word_map = {} def consume_data_from_file(file_name, consumer): for line in file(file_name): consumer.send(line) def consume_words(consumer): while True: line = yield for word in (w for w in line.split() if w.strip()): consumer.send(word) def count_words_consumer(): while True: word = yield if word not in word_map: word_map[word] = 0 word_map[word] += 1 print word_map if __name__ == '__main__': cons = count_words_consumer() cons.next() cons_inner = consume_words(cons) cons_inner.next() c = consume_data_from_file('test.txt', cons_inner) print word_map |
上面的程式碼中,真正的資料消費者是count_words_consumer,最原始的資料生產者是consume_data_from_file,資料的流向是主動從生產者推向消費者。不過上面第22、24行分別呼叫了兩次next,這個可以使用一個decorator封裝一下。
1 2 3 4 5 6 7 8 9 |
def consumer(func): def wrapper(*args,**kw): gen = func(*args, **kw) gen.next() return gen wrapper.__name__ = func.__name__ wrapper.__dict__ = func.__dict__ wrapper.__doc__ = func.__doc__ return wrapper |
修改後的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
def consumer(func): def wrapper(*args,**kw): gen = func(*args, **kw) gen.next() return gen wrapper.__name__ = func.__name__ wrapper.__dict__ = func.__dict__ wrapper.__doc__ = func.__doc__ return wrapper word_map = {} def consume_data_from_file(file_name, consumer): for line in file(file_name): consumer.send(line) @consumer def consume_words(consumer): while True: line = yield for word in (w for w in line.split() if w.strip()): consumer.send(word) @consumer def count_words_consumer(): while True: word = yield if word not in word_map: word_map[word] = 0 word_map[word] += 1 print word_map if __name__ == '__main__': cons = count_words_consumer() cons_inner = consume_words(cons) c = consume_data_from_file('test.txt', cons_inner) print word_map example_with_deco |
generator throw
除了next和send方法,generator還提供了兩個實用的方法,throw和close,這兩個方法加強了caller對generator的控制。send方法可以傳遞一個值給generator,throw方法在generator掛起的地方丟擲異常,close方法讓generator正常結束(之後就不能再呼叫next send了)。下面詳細介紹一下throw方法。
1 |
throw(type[, value[, traceback]]) |
在generator yield的地方丟擲type型別的異常,並且返回下一個被yield的值。如果type型別的異常沒有被捕獲,那麼會被傳給caller。另外,如果generator不能yield新的值,那麼向caller丟擲StopIteration異常:
1 2 3 4 5 6 7 8 9 10 11 12 |
@consumer def gen_throw(): value = yield try: yield value except Exception, e: yield str(e) # 如果註釋掉這行,那麼會丟擲StopIteration if __name__ == '__main__': g = gen_throw() assert g.send(5) == 5 assert g.throw(Exception, 'throw Exception') == 'throw Exception' |
第一次呼叫send,程式碼返回value(5)之後在第5行掛起, 然後generator throw之後會被第6行catch住。如果第7行沒有重新yield,那麼會重新丟擲StopIteration異常。
注意事項
如果一個生成器已經通過send開始執行,那麼在其再次yield之前,是不能從其他生成器再次排程到該生成器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@consumer def funcA(): while True: data = yield print 'funcA recevie', data fb.send(data * 2) @consumer def funcB(): while True: data = yield print 'funcB recevie', data fa.send(data * 2) fa = funcA() fb = funcB() if __name__ == '__main__': fa.send(10) |
輸出:
funcA recevie 10
funcB recevie 20
ValueError: generator already executing
Generator 與 Coroutine
回到Coroutine,可參見維基百科解釋,而我自己的理解比較簡單(或者片面):程式設計師可控制的併發流程,不管是程式還是執行緒,其切換都是作業系統在排程,而對於協程,程式設計師可以控制什麼時候切換出去,什麼時候切換回來。協程比程式 執行緒輕量級很多,較少了上下文切換的開銷。另外,由於是程式設計師控制排程,一定程度上也能避免一個任務被中途中斷.。協程可以用在哪些場景呢,我覺得可以歸納為非阻塞等待的場景,如遊戲程式設計,非同步IO,事件驅動。
Python中,generator的send和throw方法使得generator很像一個協程(coroutine), 但是generator只是一個半協程(semicoroutines),python doc是這樣描述的:
“All of this makes generator functions quite similar to coroutines; they yield multiple times, they have more than one entry point and their execution can be suspended. The only difference is that a generator function cannot control where should the execution continue after it yields; the control is always transferred to the generator’s caller.”
儘管如此,利用enhanced generator也能實現更強大的功能。比如上文中提到的yield_dec的例子,只能被動的等待時間到達之後繼續執行。在某些情況下比如觸發了某個事件,我們希望立即恢復執行流程,而且我們也關心具體是什麼事件,這個時候就需要在generator send了。另外一種情形,我們需要終止這個執行流程,那麼刻意呼叫close,同時在程式碼裡面做一些處理,虛擬碼如下:
1 2 3 4 5 6 7 8 |
@yield_dec def do(a): print 'do', a try: event = yield 5 print 'post_do', a, event finally: print 'do sth' |
至於之前提到的另一個例子,服務(程式)之間的非同步呼叫,也是非常適合實用協程的例子。callback的方式會割裂程式碼,把一段邏輯分散到多個函式,協程的方式會好很多,至少對於程式碼閱讀而言。其他語言,比如C#、Go語言,協程都是標準實現,特別對於go語言,協程是高併發的基石。在python3.x中,通過asyncio和async\await也增加了對協程的支援。在筆者所使用的2.7環境下,也可以使用greenlet,之後會有博文介紹。
參考
- https://www.python.org/dev/peps/pep-0342/
- http://www.dabeaz.com/coroutines/
- https://en.wikipedia.org/wiki/Coroutine#Implementations_for_Python