深度學習Python協程

基本定義

可迭代對象

可迭代對象(Iterable):可以直接作用於for循環的對象統稱爲可迭代對象。可以使用isinstance()判斷一個對象是否是Iterable對象。

>>> from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False

迭代器

迭代器(Iterator): PythonIterator對象表示的是一個數據流,Iterator對象可以被next()函數調用並不斷返回下一個數據,直到沒有數據時拋出StopIteration錯誤。可以把這個數據流看做是一個有序序列,但我們卻不能提前知道序列的長度,只能不斷通過next()函數實現按需計算下一個數據,所以Iterator的計算是惰性的,只有在需要返回下一個數據時它纔會計算。

Iterator甚至可以表示一個無限大的數據流,例如全體自然數。而使用list是永遠不可能存儲全體自然數的。

生成器

生成器(generator):生成器不但可以作用於for循環,還可以被next()函數不斷調用並返回下一個值,直到最後拋出StopIteration錯誤表示無法繼續返回下一個值了。我們可以使用isinstance()判斷一個對象是否是Iterator對象:

>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False

yield

牛津詞典中對yield定義一般有2層含義:產出讓步 ,對於Python來講如果我們在一段代碼中使用yield value確實這2層含義都成立。首先,yield value確實會產出value給到調用next(...)方法的調用方;並且還會作出讓步,暫停執行yield value之後代碼,讓步給調用方繼續執行。直到調用方需要下一個值時再次調用next(...)方法。調用方會從yield value中取到value

如果我們在代碼中使用var = yield也可以從調用方獲取到數據,不過需要調用方調用.send(data)將數據傳輸到var而不是next(...)方法。

yield關鍵字甚至可以不接受或者產出數據,不管數據如何流動,yield都是一種流程控制工具,使用它可以實現協作式多任務;協程可以把控制器讓步給中心調度程序,從而激活其他協程。

協程基本案例

下面是一段simple_coroutine協程和main交互切換執行的過程演示,simple_coroutine協程和main只要有一方隨機產出6則終止執行,否則就會一直切換執行

import random
def simple_coroutine():
    print('coroutine started')
    while True:
        send = random.choice(range(8))
        print('coroutine send',send)
        if send == 6:
            yield send
            break

        receive = yield send
        print('coroutine receive', receive)

if __name__ == '__main__':
    print('main started')
    coroutine = simple_coroutine()
    print('main declare',coroutine)
    receive = next(coroutine)
    print('main next',receive)
    while receive != 6:
        send = random.choice(range(8))
        print('main send', send)
        if send == 6:
            coroutine.send(send)
            coroutine.close()
            break
        receive = coroutine.send(send)
        print('main receive',receive)

結果如下:

#/usr/local/bin/python3.7 /data/code/python/test/coroutine/coroutinue.py
main started
main declare <generator object simple_coroutine at 0x10c862480>
coroutine started
coroutine send 6
main next 6

或者

#/usr/local/bin/python3.7 /data/code/python/test/coroutine/coroutinue.py
main started
main declare <generator object simple_coroutine at 0x10c1d8480>
coroutine started
coroutine send 0
main next 0
main send 1
coroutine receive 1
coroutine send 3
main receive 3
main send 2
coroutine receive 2
coroutine send 6
main receive 6

通過上面的執行結果我們可以得到的結論是:

  • 協程使用生成器的函數定義:定義體中有yield關鍵字。
  • 帶有 yield 的函數不再是一個普通函數,Python 解釋器會將其視爲一個 generator
  • coroutine = simple_coroutine()與創建生成器的方式一樣,調用函數只會得到生成器對象<generator object simple_coroutine at 0x10c1d8480>,並不會開始執行協程代碼。
  • 我們需要先調用next(coroutine)函數,因爲得到的生成器對象還沒啓動沒在yield處暫停,我們無法調用coroutine.send(send)發送數據。
  • 當我們調用next(coroutine)函數後,得到的生成器對象會啓動執行到yield send處產出receive,然後終止執行,讓步給調用方main繼續執行,直到調用方main需要下一個值時調用receive = coroutine.send(send)讓步給協程繼續執行。
  • value = yield代表協程只需要從調用方接受數據,那麼產出的值爲None,這個值是隱式指定的,因爲yield右邊沒有關鍵字。

協程狀態

協程可以處於下面的四種狀態,協程當前的狀態可以使用inspect.getgeneratorstate(...)函數得到:

  • GEN_CREATE:創建,等待開始執行
  • GEN_RUNNING:生成器執行中
  • GEN_SUSPENDED:在yield表達式處暫停
  • GEN_CLOSE:執行結束,生成器關閉

此時,我們將main修改如下:

if __name__ == '__main__':
    print('main started')
    coroutine = simple_coroutine()
    print('main declare',coroutine)
    print(getgeneratorstate(coroutine))
    receive = next(coroutine)
    print('main next',receive)
    while receive != 6:
        send = random.choice(range(8))
        print('main send', send)
        if send == 6:
            coroutine.send(send)
            print(getgeneratorstate(coroutine))
            coroutine.close()
            print(getgeneratorstate(coroutine))
            break
        receive = coroutine.send(send)
        print(getgeneratorstate(coroutine))
        print('main receive',receive)

執行結果如下:

#/usr/local/bin/python3.7 /data/code/python/test/coroutine/coroutinue.py
main started
main declare <generator object simple_coroutine at 0x10ca96480>
GEN_CREATED
coroutine started
coroutine send 7
main next 7
main send 6
coroutine receive 6
coroutine send 5
GEN_SUSPENDED
GEN_CLOSED

或者

/usr/local/bin/python3.7 /data/code/python/test/coroutine/coroutinue.py
main started
main declare <generator object simple_coroutine at 0x10effc480>
GEN_CREATED
coroutine started
coroutine send 3
main next 3
main send 2
coroutine receive 2
coroutine send 2
GEN_SUSPENDED
main receive 2
main send 7
coroutine receive 7
coroutine send 6
GEN_SUSPENDED
main receive 6

從上面的執行結果來看,我們可以得到一下結論

  • coroutine = simple_coroutine()創建協程,協程處於GEN_CREATED狀態,等待開始執行
  • 調用next(coroutine)後,協程處於GEN_RUNNING執行中,直到遇到yield產出值後處於GEN_SUSPENDED暫停狀態
  • 協程break執行完成或者調用coroutine.close()後,協程處於GEN_CLOSE:執行結束關閉狀態。

使用協程連續計算平均值

下面我們看一個使用協程連續計算平均值的例子,我們設置一個無限循環,只要調用方不斷的將值發給協程,他就會一直接受值,然後計算total和count。僅當調用方調用.close()方法,或者程序沒有對協程的引用時被垃圾程序回收。

def average():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count


if __name__ == '__main__':
    coro_avg = average()
    next(coro_avg)
    no_list = []
    for i in range(6):
        no =random.choice(range(100))
        no_list.append(no)
        print(no_list,' avg:  ',coro_avg.send(no))

執行結果

#/usr/local/bin/python3.7 /data/code/python/test/coroutine/coroutinue.py
[51]  avg:   51.0
[51, 45]  avg:   48.0
[51, 45, 44]  avg:   46.666666666666664
[51, 45, 44, 81]  avg:   55.25
[51, 45, 44, 81, 49]  avg:   54.0
[51, 45, 44, 81, 49, 50]  avg:   53.333333333333336

協程返回值

爲了使協程返回值,我們必須要使協程可以正常終止。我們改造上面計算連續平均值的程序。

from collections import namedtuple

Result = namedtuple('Result','count average')

def average():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        if term is None:
            break;
        total += term
        count += 1
        average = total/count
    return Result(count,average)

if __name__ == '__main__':
    coro_avg = average()
    next(coro_avg)
    no_list = []
    for i in range(6):
        no =random.choice(range(100))
        no_list.append(no)
        coro_avg.send(no)

    try:
        coro_avg.send(None)
    except StopIteration as e:
        result = e.value

    print(no_list,' avg:  ',result)

執行結果如下:

#/usr/local/bin/python3.7 /data/code/python/test/coroutine/coroutinue.py
[71, 2, 73, 55, 74, 67]  avg:   Result(count=6, average=57.0)

yield from

  • yield from語法可以讓我們方便地調用另一個generator
  • yield from Iterable 相當於for i in Iterable:yield i
  • yield from 結果會在內部自動捕獲StopIteration 異常。這種處理方式與 for 循環處理StopIteration異常的方式一樣。對於yield from 結構來說,解釋器不僅會捕獲StopIteration異常,還會把value屬性的值變成yield from 表達式的值。
  • 在函數外部不能使用yield fromyield也不行)。
def gen():
    for c in "AB":
        yield c
    for i in range(1,3):
        yield i

def gen2():
    yield from "AB"
    yield from range(1,3)

def gen3():
    yield from gen()
    
if __name__ == '__main__':
    l_result = []
    coro = gen()
    while True:
        try:
            a = next(coro)
            l_result.append(a)
        except StopIteration as e:
            break

    print(l_result)

    print(list(gen3()))

    print(list(gen()))
    print(list(gen2()))

執行結果

#/usr/local/bin/python3.7 /data/code/python/test/coroutine/coroutinue.py
['A', 'B', 1, 2]
['A', 'B', 1, 2]
['A', 'B', 1, 2]
['A', 'B', 1, 2]

yield from與同步非阻塞

yield from本身只是讓我們方便地調用另一個generator,但是在阻塞網絡的情況下,我們可以利用yield fromasyncio實現異步非阻塞。

#複雜計算要一會
@asyncio.coroutine
def count_no():
    return 2**1000

@asyncio.coroutine
def count(no):
    print(time.time(),'count %d! %s' % (no,threading.currentThread()))
    yield from count_no()
    print(time.time(),'count %d end! %s' % (no,threading.currentThread()))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks=[count(i) for i in range(100)]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

執行結果如下:

#/usr/local/bin/python3.7 /data/code/python/test/coroutine/coroutinue.py
1545651898.067989 count 9! <_MainThread(MainThread, started 140736385631168)>
1545651898.068131 count 9 end! <_MainThread(MainThread, started 140736385631168)>
1545651898.068198 count 75! <_MainThread(MainThread, started 140736385631168)>
1545651898.06828 count 75 end! <_MainThread(MainThread, started 140736385631168)>
1545651898.0683289 count 10! <_MainThread(MainThread, started 140736385631168)>
1545651898.068373 count 10 end! <_MainThread(MainThread, started 140736385631168)>
1545651898.068413 count 76! <_MainThread(MainThread, started 140736385631168)>
1545651898.068458 count 76 end! <_MainThread(MainThread, started 140736385631168)>
1545651898.068498 count 11! <_MainThread(MainThread, started 140736385631168)>
1545651898.068539 count 11 end! <_MainThread(MainThread, started 140736385631168)>
1545651898.068577 count 77! <_MainThread(MainThread, started 140736385631168)>
1545651898.0689468 count 77 end! <_MainThread(MainThread, started 140736385631168)>
1545651898.0690439 count 12! <_MainThread(MainThread, started 140736385631168)>
1545651898.069103 count 12 end! <_MainThread(MainThread, started 140736385631168)>
1545651898.069149 count 78! <_MainThread(MainThread, started 140736385631168)>
1545651898.069193 count 78 end! <_MainThread(MainThread, started 140736385631168)>
1545651898.0692348 count 13! <_MainThread(MainThread, started 140736385631168)>
1545651898.069278 count 13 end! <_MainThread(MainThread, started 140736385631168)>
1545651898.0693178 count 79! <_MainThread(MainThread, started 140736385631168)>
1545651898.069359 count 79 end! <_MainThread(MainThread, started 140736385631168)>

複雜計算操作可以達到我們的目標,但是網絡請求呢?

import asyncio
import requests
import threading
import time

#asyncio.coroutine包裝成generator
@asyncio.coroutine
def request_net(url):
    return requests.get(url)

@asyncio.coroutine
def hello(url):
    print(time.time(),'Hello %s! %s' % (url,threading.currentThread()))
    resp = yield from request_net(url)
    print(time.time(),resp.request.url)
    print(time.time(),'Hello %s again! %s' % (url,threading.currentThread()))


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    #訪問12306模擬網絡長時間操作
    tasks = [hello('https://kyfw.12306.cn'), hello('http://www.baidu.com')]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

執行結果如下

#/usr/local/bin/python3.7 /data/code/python/test/coroutine/coroutinue.py
1545649725.3060732 Hello https://kyfw.12306.cn! <_MainThread(MainThread, started 140736385631168)>
1545649725.96431 https://kyfw.12306.cn/otn/passport?redirect=/otn/
1545649725.96435 Hello https://kyfw.12306.cn again! <_MainThread(MainThread, started 140736385631168)>
1545649725.9646132 Hello http://www.baidu.com! <_MainThread(MainThread, started 140736385631168)>
1545649726.023788 http://www.baidu.com/
1545649726.023826 Hello http://www.baidu.com again! <_MainThread(MainThread, started 140736385631168)>

可以發現和正常的請求並沒有什麼兩樣,依然還是順次執行的,12306那麼卡,百度也是等待返回後順序調度的,其實,要實現異步處理,我們必須要使用支持異步操作的請求方式纔可以實現真正的異步

async def get(url):
    async with aiohttp.ClientSession() as session:
        rsp = await session.get(url)
        result = await rsp.text()
        return result


async def request(url):
    print(time.time(), 'Hello %s! %s' % (url, threading.currentThread()))
    result = await get(url)
    print(time.time(),'Get response from', url)
    print(time.time(), 'Hello %s again! %s' % (url, threading.currentThread()))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [ asyncio.ensure_future(request('http://www.163.com/')),asyncio.ensure_future(request('http://www.baidu.com'))]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

執行結果如下:

#/usr/local/bin/python3.7 /data/code/python/test/coroutine/coroutinue.py
1545651249.1526 Hello http://www.163.com/! <_MainThread(MainThread, started 140736385631168)>
1545651249.1627488 Hello http://www.baidu.com! <_MainThread(MainThread, started 140736385631168)>
1545651250.150497 Get response from http://www.baidu.com
1545651250.1505191 Hello http://www.baidu.com again! <_MainThread(MainThread, started 140736385631168)>
1545651250.165774 Get response from http://www.163.com/
1545651250.165801 Hello http://www.163.com/ again! <_MainThread(MainThread, started 140736385631168)>

asyncio提供的@asyncio.coroutine可以把一個generator標記爲coroutine類型,然後在coroutine內部用yield from調用另一個coroutine實現異步操作。
爲了簡化並更好地標識異步IO,從Python 3.5開始引入了新的語法asyncawait,可以讓coroutine的代碼更簡潔易讀。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章