1. 程式人生 > >python enhanced generator - coroutine

python enhanced generator - coroutine

其他 back final itl ron 一段 返回 multiple 會有

  本文主要介紹python中Enhanced generator即coroutine相關內容,包括基本語法、使用場景、註意事項,以及與其他語言協程實現的異同。

enhanced generator

  在上文介紹了yield和generator的使用場景和主意事項,只用到了generator的next方法,事實上generator還有更強大的功能。PEP 342為generator增加了一系列方法來使得generator更像一個協程Coroutine。做主要的變化在於早期的yield只能返回值(作為數據的產生者), 而新增加的send方法能在generator恢復的時候消費一個數值,而去caller(generator的調用著)也可以通過throw在generator掛起的主動拋出異常。

  首先看看增強版本的yield,語法格式如下:
  back_data = yield cur_ret

  這段代碼的意思是:當執行到這條語句時,返回cur_ret給調用者;並且當generator通過next()或者send(some_data)方法恢復的時候,將some_data賦值給back_data.For example:

技術分享
 1 def gen(data):
 2     print ‘before yield‘, data
 3     back_data = yield data
 4     print ‘after resume‘, back_data
 5     
 6 if __name__ == ‘__main__‘:
 7     g = gen(1)
 8     print g.next()
 9     try:
10         g.send(0)
11     except StopIteration:
12         pass
技術分享

輸出:
before yield 1
1
after resume 0

兩點需要註意

(1) next() 等價於 send(None)
(2) 第一次調用時,需要使用next()語句或是send(None),不能使用send發送一個非None的值,否則會出錯的,因為沒有Python yield語句來接收這個值。

應用場景

  當generator可以接受數據(在從掛起狀態恢復的時候) 而不僅僅是返回數據時, generator就有了消費數據(push)的能力。下面的例子來自這裏:

技術分享
 1 word_map = {}
 2 def consume_data_from_file(file_name, consumer):
 3     for line in file(file_name):
 4         consumer.send(line)
 5 
 6 def consume_words(consumer):
 7     while True:
 8         line = yield
 9         for word in (w for w in line.split() if w.strip()):
10             consumer.send(word)
11 
12 def count_words_consumer():
13     while True:
14         word  = yield
15         if word not in word_map:
16             word_map[word] = 0
17         word_map[word] += 1
18     print word_map
19 
20 if __name__ == ‘__main__‘:
21     cons = count_words_consumer()
22     cons.next()
23     cons_inner = consume_words(cons)
24     cons_inner.next()
25     c = consume_data_from_file(‘test.txt‘, cons_inner)
26     print word_map
技術分享

  上面的代碼中,真正的數據消費者是count_words_consumer,最原始的數據生產者是consume_data_from_file,數據的流向是主動從生產者推向消費者。不過上面第22、24行分別調用了兩次next,這個可以使用一個decorator封裝一下。

技術分享
1 def consumer(func):
2     def wrapper(*args,**kw):
3         gen = func(*args, **kw)
4         gen.next()
5         return gen
6     wrapper.__name__ = func.__name__
7     wrapper.__dict__ = func.__dict__
8     wrapper.__doc__  = func.__doc__
9     return wrapper
技術分享

修改後的代碼

技術分享 example_with_deco

generator throw

  除了next和send方法,generator還提供了兩個實用的方法,throw和close,這兩個方法加強了caller對generator的控制。send方法可以傳遞一個值給generator,throw方法在generator掛起的地方拋出異常,close方法讓generator正常結束(之後就不能再調用next send了)。下面詳細介紹一下throw方法。

throw(type[, value[, traceback]])
  在generator yield的地方拋出type類型的異常,並且返回下一個被yield的值。如果type類型的異常沒有被捕獲,那麽會被傳給caller。另外,如果generator不能yield新的值,那麽向caller拋出StopIteration異常

技術分享
 1 @consumer
 2 def gen_throw():
 3     value = yield 
 4     try:
 5         yield value
 6     except Exception, e:
 7         yield str(e) # 如果註釋掉這行,那麽會拋出StopIteration
 8 
 9 if __name__ == ‘__main__‘:
10     g = gen_throw()
11     assert g.send(5) == 5
12     assert g.throw(Exception, ‘throw Exception‘) == ‘throw Exception‘
技術分享

  第一次調用send,代碼返回value(5)之後在第5行掛起, 然後generator throw之後會被第6行catch住。如果第7行沒有重新yield,那麽會重新拋出StopIteration異常。

 

註意事項

  如果一個生成器已經通過send開始執行,那麽在其再次yield之前,是不能從其他生成器再次調度到該生成器

技術分享
 1 @consumer
 2 def funcA():
 3     while True:
 4         data = yield
 5         print ‘funcA recevie‘, data
 6         fb.send(data * 2)
 7 
 8 @consumer
 9 def funcB():
10     while True:
11         data = yield
12         print ‘funcB recevie‘, data
13         fa.send(data * 2)
14 
15 fa = funcA()
16 fb = funcB()
17 if __name__ == ‘__main__‘:
18     fa.send(10)
技術分享

輸出:

funcA recevie 10
funcB recevie 20
ValueError: generator already executing

Generator 與 Coroutine

  回到Coroutine,可參見維基百科解釋(https://en.wikipedia.org/wiki/Coroutine#Implementations_for_Python),而我自己的理解比較簡單(或者片面):程序員可控制的並發流程,不管是進程還是線程,其切換都是操作系統在調度,而對於協程,程序員可以控制什麽時候切換出去,什麽時候切換回來。協程比進程 線程輕量級很多,較少了上下文切換的開銷。另外,由於是程序員控制調度,一定程度上也能避免一個任務被中途中斷.。協程可以用在哪些場景呢,我覺得可以歸納為非阻塞等待的場景,如遊戲編程,異步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 @yield_dec
2 def do(a):
3     print ‘do‘, a
4     try:
5         event = yield 5
6         print ‘post_do‘, a, event
7     finally:
8         print ‘do sth‘
技術分享

  至於之前提到的另一個例子,服務(進程)之間的異步調用,也是非常適合實用協程的例子。callback的方式會割裂代碼,把一段邏輯分散到多個函數,協程的方式會好很多,至少對於代碼閱讀而言。其他語言,比如C#、Go語言,協程都是標準實現,特別對於go語言,協程是高並發的基石。在python3.x中,通過asyncio和async\await也增加了對協程的支持。在筆者所使用的2.7環境下,也可以使用greenlet,之後會有博文介紹。

References:

https://www.python.org/dev/peps/pep-0342/
http://www.dabeaz.com/coroutines/
https://en.wikipedia.org/wiki/Coroutine#Implementations_for_Python

python enhanced generator - coroutine