生成器與yield
函數使用yield關鍵字可以定義生成器對象。生成器是一個函數。它生成一個值的序列,以便在迭代中使用,例如:
1 def countdown(n): 2 print('倒計時:%s' % n) 3 while n > 0: 4 yield n 5 n -= 1 6 return 7 8 c = countdown(10)
如果調用該函數,就會發現其中的代碼不會開始執行,相反它會返回一個生成器對象,接着該生成器對象就會在__next__()被調用時執行函數。
print(c.__next__()) print(c.__next__()) print(c.__next__())
調用__next__()時,生成器函數將開始執行語句,知道遇到yield語句為止。yield語句在函數執行停止的地方生成一個結果,直到再次調用next()。然后繼續執行yield()之后的語句。通常不會在生成器上直接調用next()方法,而是通過for語句、sum()或一些消耗序列的其他操作使用生成器。例如:
for i in countdown(10): print(i) a = sum(countdown(10)) print(a)
生成器函數完成的標志是返回或引發StopIteration異常,這標志着迭代的結束。如果生成器在完成時返回None以外的值都是不合法的。生成器使用時存在一個棘手的問題,即生成器函數僅被部分消耗,例如:
for n in countdown(10): if n == 2: break print(n)
在這個例子中,通過調用break退出循環,而相關的生成器也沒有全部完成。為了處理這種情況,生成器對象提供方法close()標識關閉。不再使用或刪除生成器時,就會調用close()方法。通常不必手動調用close()方法,但也可以這么做。
在生成器函數內部,在yield語句上出現GeneratorExit異常時就會調用close()方法。也可以選擇捕捉這個異常,以便執行清理操作
1 def countdown2(n): 2 print('倒計時:%s' % n) 3 try: 4 while n > 0: 5 yield n 6 n -= 1 7 except GeneratorExit: 8 print('GeneratorExit %s' % n) 9 10 c = countdown2(2) 11 12 print(next(c)) 13 print(next(c)) 14 del c
雖然可以捕捉GenratorExit異常,但對於生成器函數而言,使用yield語句處理異常並生成另一個輸出值是不合法的。另外,如果程序當前正在對生成器進行迭代,不應該通過另一個的執行線程或從信號處理程序異步調用該生成器上的close()方法。
協程與yield表達式
在函數內, yield語句還可以作為表達式使用,出現在賦值運算符的右邊,例如:
def receive(): print('Ready to receive') while True: n = yield print('Got %s' % n)
以這種方式使用yield語句的函數稱為協程,向函數發送值時函數將執行。它的行為也十分類似於生成器
r = receive() r.__next__() r.send(1)
在這個例子中,一開始調用__next__()是必不可少的,這樣協程才能執行第一個yield表達式之前的語句。這時,協程會掛起,等待相關生成器對象r的send()方法給他發送一個值。
傳遞給send()的值由協程中的yield表達式返回。接收到值后,協程就會執行語句,直到遇到下一條yield語句。
在協程中需要調用next()這件事很容易被忽略,這經常稱為錯誤出現的原因。因此,建議使用一個能夠自動完成該步驟的裝飾器來包裝協程。
1 def coroution(func): 2 def start(*args, **kwargs): 3 g = func(*args, **kwargs) 4 g.__next__() 5 return g 6 return start 7 8 9 # 使用這個裝飾器就可以像下面這樣編寫和使用協程: 10 @coroution 11 def receive(): 12 print('Ready to receive') 13 while True: 14 n = yield 15 print('Go %s' % n) 16 17 18 r = receive() 19 r.send('hello world') # 無需初始調用.next()方法
協程一般會不斷地執行下去,除非被顯式關閉或者自己退出。關閉后如果繼續給協程發送值就會引發StopIteration異常。正如前面關於生成器的內容中講到的那樣,close()操作將在協程內部引發GeneratorExit異常。
可以使用throw(exctype [, value [.tb]])方法在協程內部引發異常,其中exctype是指異常類型,value是指異常的值,而tb是指跟蹤對象例如:
r.throw(RuntimeError, "You're hosed")
以這種方式引發的異常將在協程中當前執行的yield語句處出現。協程可以選擇捕捉異常並以正確方式處理它們。使用throw()方法作為給協程的異步信號並不安全--永遠都不應該通過單獨的執行線程或信號處理程序調用這個方法。
如果yield表達式中提供了值,協程可以使用yield語句同時接收和發出返回值,例如:
def line_splitter(delimiter=None): print("Ready to split") result = None while True: line = yield result result = line.split(delimiter) l = line_splitter(',') l.__next__() print(l.send("a,b,c"))
首個__next__()調用讓協程向前執行到yield result,這將返回result的值None。在接下來的send()調用中,接收到的值被放在line中並拆分到result中。
send()方法的返回值就是傳遞給下一條yield語句的值。換句話說,send()方法的返回值來自下一個yield表達式,而不是接收send()傳遞的值的yield表達式。
如果協程返回值,需要小心處理使用throw()引發的異常。如果使用throw()在協程中引發一個異常,傳遞給協程中下一條yield語句的值將作為throw()
方法的結果返回。如果需要這個值卻又忘記保存它,它就會消失不見。
yield from
yield from 是在Python3.3才出現的語法,后面需要加的是可迭代對象。
a = 'qwertt' def str_to_list(): yield from a def str_to_list2(): for i in a: yield i print(list(str_to_list())) print(list(str_to_list2()))
yield from 的主要功能是打開雙向通道,把最外層的調用方與最內層的子生成器連接起來,這樣兩者可以直接發送和產出值,還可以傳入異常
而不用在位於中間的協程中添加大量處理異常的樣板代碼。
雙向通道: 調用方通過send()直接發送信息給子生成器,而子生成器yield的值,也直接返回給調用方
從Python 3.5開始引入了新的語法 async
和 await
,而await替代的就是yield from