python協程1:yield 10分鐘入門

最近找到一本python好書《流暢的python》,是到如今爲止看到的對python高級特性講述最詳細的一本。
看了協程一章,作個讀書筆記,加深印象。python

協程定義

協程的底層架構是在pep342 中定義,並在python2.5 實現的。多線程

python2.5 中,yield關鍵字能夠在表達式中使用,並且生成器API中增長了 .send(value)方法。生成器可使用.send(...)方法發送數據,發送的數據會成爲生成器函數中yield表達式的值。架構

協程是指一個過程,這個過程與調用方協做,產出有調用方提供的值。所以,生成器能夠做爲協程使用。函數

除了 .send(...)方法,pep342 和添加了 .throw(...)(讓調用方拋出異常,在生成器中處理)和.close()(終止生成器)方法。ui

python3.3後,pep380對生成器函數作了兩處改動:spa

  • 生成器能夠返回一個值;之前,若是生成器中給return語句提供值,會拋出SyntaxError異常。線程

  • 引入yield from 語法,使用它能夠把複雜的生成器重構成小型的嵌套生成器,省去以前把生成器的工做委託給子生成器所需的大量模板代碼。code

協程生成器的基本行爲

首先說明一下,協程有四個狀態,可使用inspect.getgeneratorstate(...)函數肯定:orm

  • GEN_CREATED # 等待開始執行協程

  • GEN_RUNNING # 解釋器正在執行(只有在多線程應用中才能看到這個狀態)

  • GEN_SUSPENDED # 在yield表達式處暫停

  • GEN_CLOSED # 執行結束

#! -*- coding: utf-8 -*-
import inspect

# 協程使用生成器函數定義:定義體中有yield關鍵字。
def simple_coroutine():
    print('-> coroutine started')
    # yield 在表達式中使用;若是協程只須要從客戶那裏接收數據,yield關鍵字右邊不須要加表達式(yield默認返回None)
    x = yield
    print('-> coroutine received:', x)


my_coro = simple_coroutine()
my_coro # 和建立生成器的方式同樣,調用函數獲得生成器對象。
# 協程處於 GEN_CREATED (等待開始狀態)
print(inspect.getgeneratorstate(my_coro))

my_coro.send(None)
# 首先要調用next()函數,由於生成器尚未啓動,沒有在yield語句處暫停,因此開始沒法發送數據
# 發送 None 能夠達到相同的效果 my_coro.send(None) 
next(my_coro)
# 此時協程處於 GEN_SUSPENDED (在yield表達式處暫停)
print(inspect.getgeneratorstate(my_coro))

# 調用這個方法後,協程定義體中的yield表達式會計算出42;如今協程會恢復,一直運行到下一個yield表達式,或者終止。
my_coro.send(42)
print(inspect.getgeneratorstate(my_coro))

運行上述代碼,輸出結果以下

GEN_CREATED
-> coroutine started
GEN_SUSPENDED
-> coroutine received: 42

# 這裏,控制權流動到協程定義體的尾部,致使生成器像往常同樣拋出StopIteration異常
Traceback (most recent call last):
  File "/Users/gs/coroutine.py", line 18, in <module> 
    my_coro.send(42)
StopIteration

send方法的參數會成爲暫停yield表達式的值,因此,僅當協程處於暫停狀態是才能調用send方法。
若是協程還未激活(GEN_CREATED 狀態)要調用next(my_coro) 激活協程,也能夠調用my_coro.send(None)

若是建立協程對象後當即把None以外的值發給它,會出現下述錯誤:

>>> my_coro = simple_coroutine()
>>> my_coro.send(123)

Traceback (most recent call last):
  File "/Users/gs/coroutine.py", line 14, in <module>
    my_coro.send(123)
TypeError: can't send non-None value to a just-started generator

仔細看錯誤消息

can't send non-None value to a just-started generator

最早調用next(my_coro) 這一步一般稱爲」預激「(prime)協程---即,讓協程向前執行到第一個yield表達式,準備好做爲活躍的協程使用。

再看一個兩個值得協程

def simple_coro2(a):
    print('-> coroutine started: a =', a)
    b = yield a
    print('-> Received: b =', b)
    c = yield a + b
    print('-> Received: c =', c)

my_coro2 = simple_coro2(14)
print(inspect.getgeneratorstate(my_coro2))
# 這裏inspect.getgeneratorstate(my_coro2) 獲得結果爲 GEN_CREATED (協程未啓動)

next(my_coro2)
# 向前執行到第一個yield 處 打印 「-> coroutine started: a = 14」
# 而且產生值 14 (yield a 執行 等待爲b賦值)
print(inspect.getgeneratorstate(my_coro2))
# 這裏inspect.getgeneratorstate(my_coro2) 獲得結果爲 GEN_SUSPENDED (協程處於暫停狀態)

my_coro2.send(28)
# 向前執行到第二個yield 處 打印 「-> Received: b = 28」
# 而且產生值 a + b = 42(yield a + b 執行 獲得結果42 等待爲c賦值)
print(inspect.getgeneratorstate(my_coro2))
# 這裏inspect.getgeneratorstate(my_coro2) 獲得結果爲 GEN_SUSPENDED (協程處於暫停狀態)

my_coro2.send(99)
# 把數字99發送給暫停協程,計算yield 表達式,獲得99,而後把那個數賦值給c 打印 「-> Received: c = 99」
# 協程終止,拋出StopIteration

運行上述代碼,輸出結果以下

GEN_CREATED
-> coroutine started: a = 14
GEN_SUSPENDED
-> Received: b = 28
-> Received: c = 99

Traceback (most recent call last):
  File "/Users/gs/coroutine.py", line 37, in <module>
    my_coro2.send(99)
StopIteration

simple_coro2 協程的執行過程分爲3個階段,以下圖所示

  1. 調用next(my_coro2),打印第一個消息,而後執行yield a,產出數字14.

  2. 調用my_coro2.send(28),把28賦值給b,打印第二個消息,而後執行 yield a + b 產生數字42

  3. 調用my_coro2.send(99),把99賦值給c,而後打印第三個消息,協程終止。

使用裝飾器預激協程

咱們已經知道,協程若是不預激,不能使用send() 傳入非None 數據。因此,調用my_coro.send(x)以前,必定要調用next(my_coro)。
爲了簡化,咱們會使用裝飾器預激協程。

from functools import wraps

def coroutinue(func):
    '''
    裝飾器: 向前執行到第一個`yield`表達式,預激`func`
    :param func: func name
    :return: primer
    '''

    @wraps(func)
    def primer(*args, **kwargs):
        # 把裝飾器生成器函數替換成這裏的primer函數;調用primer函數時,返回預激後的生成器。
        gen = func(*args, **kwargs)
        # 調用被被裝飾函數,獲取生成器對象
        next(gen)  # 預激生成器
        return gen  # 返回生成器
    return primer


# 使用方法以下

@coroutinue
def simple_coro(a):
    a = yield

simple_coro(12)  # 已經預激

終止協程和異常處理

協程中,爲處理的異常會向上冒泡,傳遞給next函數或send方法的調用方,未處理的異常會致使協程終止。

看下邊這個例子

#! -*- coding: utf-8 -*-

from functools import wraps

def coroutinue(func):
    '''
    裝飾器: 向前執行到第一個`yield`表達式,預激`func`
    :param func: func name
    :return: primer
    '''

    @wraps(func)
    def primer(*args, **kwargs):
        # 把裝飾器生成器函數替換成這裏的primer函數;調用primer函數時,返回預激後的生成器。
        gen = func(*args, **kwargs)
        # 調用被被裝飾函數,獲取生成器對象
        next(gen)  # 預激生成器
        return gen  # 返回生成器
    return primer


@coroutinue
def averager():
    # 使用協程求平均值
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count

coro_avg = averager()
print(coro_avg.send(40))
print(coro_avg.send(50))
print(coro_avg.send('123')) # 因爲發送的不是數字,致使內部有異常拋出。

執行上述代碼結果以下

40.0
45.0
Traceback (most recent call last):
  File "/Users/gs/coro_exception.py", line 37, in <module>
    print(coro_avg.send('123'))
  File "/Users/gs/coro_exception.py", line 30, in averager
    total += term
TypeError: unsupported operand type(s) for +=: 'float' and 'str'

出錯的緣由是發送給協程的'123'值不能加到total變量上。
出錯後,若是再次調用 coro_avg.send(x) 方法 會拋出 StopIteration 異常。

由上邊的例子咱們能夠知道,若是想讓協程退出,能夠發送給它一個特定的值。好比None和Ellipsis。(推薦使用Ellipsis,由於咱們不太使用這個值)
從Python2.5 開始,咱們能夠在生成器上調用兩個方法,顯式的把異常發給協程。
這兩個方法是throw和close。

generator.throw(exc_type[, exc_value[, traceback]])

這個方法使生成器在暫停的yield表達式處拋出指定的異常。若是生成器處理了拋出的異常,代碼會向前執行到下一個yield表達式,而產出的值會成爲調用throw方法獲得的返回值。若是沒有處理,則向上冒泡,直接拋出。

generator.close()

生成器在暫停的yield表達式處拋出GeneratorExit異常。
若是生成器沒有處理這個異常或者拋出了StopIteration異常,調用方不會報錯。若是收到GeneratorExit異常,生成器必定不能產出值,不然解釋器會拋出RuntimeError異常。

示例: 使用close和throw方法控制協程。

import inspect


class DemoException(Exception):
    pass


@coroutinue
def exc_handling():
    print('-> coroutine started')
    while True:
        try:
            x = yield
        except DemoException:
            print('*** DemoException handled. Conginuing...')
        else:
            # 若是沒有異常顯示接收到的值
            print('--> coroutine received: {!r}'.format(x))
    raise RuntimeError('This line should never run.')  # 這一行永遠不會執行 


exc_coro = exc_handling()

exc_coro.send(11)
exc_coro.send(12)
exc_coro.send(13)
exc_coro.close()
print(inspect.getgeneratorstate(exc_coro))

raise RuntimeError('This line should never run.') 永遠不會執行,由於只有未處理的異常纔會終止循環,而一旦出現未處理的異常,協程會當即終止。

執行上述代碼獲得結果爲:

-> coroutine started
--> coroutine received: 11
--> coroutine received: 12
--> coroutine received: 13
GEN_CLOSED    # 協程終止

上述代碼,若是傳入DemoException,協程不會停止,由於作了異常處理。

exc_coro = exc_handling()

exc_coro.send(11)
exc_coro.send(12)
exc_coro.send(13)
exc_coro.throw(DemoException) # 協程不會停止,可是若是傳入的是未處理的異常,協程會終止
print(inspect.getgeneratorstate(exc_coro))
exc_coro.close()
print(inspect.getgeneratorstate(exc_coro))

## output

-> coroutine started
--> coroutine received: 11
--> coroutine received: 12
--> coroutine received: 13
*** DemoException handled. Conginuing...
GEN_SUSPENDED
GEN_CLOSED

若是無論協程如何結束都想作些處理工做,要把協程定義體重的相關代碼放入try/finally塊中。

@coroutinue
def exc_handling():
    print('-> coroutine started')
    try:
        while True:
            try:
                x = yield
            except DemoException:
                print('*** DemoException handled. Conginuing...')
            else:
                # 若是沒有異常顯示接收到的值
                print('--> coroutine received: {!r}'.format(x))
    finally:
        print('-> coroutine ending')

上述部分介紹了:

  • 生成器做爲協程使用時的行爲和狀態

  • 使用裝飾器預激協程

  • 調用方如何使用生成器對象的 .throw(...)和.close() 方法控制協程

下一部分將介紹:

  • 協程終止時如何返回值

  • yield新句法的用途和語義

最後,感謝女友支持。

>歡迎關注 >請我喝芬達
歡迎關注 請我喝芬達
相關文章
相關標籤/搜索