本文參考了:html
先讓咱們來學習或者回顧一下yield
和yield from
的用法。若是你很自信本身完成理解了,能夠跳到下一部分。python
Python3.3提出了一種新的語法:yield from
。git
yield from iterator
複製代碼
本質上也就至關於:github
for x in iterator:
yield x
複製代碼
下面的這個例子中,兩個 yield from
加起來,就組合獲得了一個大的iterable
(例子來源於官網3.3 release):golang
>>> def g(x):
... yield from range(x, 0, -1)
... yield from range(x)
...
>>> list(g(5))
[5, 4, 3, 2, 1, 0, 1, 2, 3, 4]
複製代碼
理解 yield from
對於接下來的部分相當重要。想要徹底理解 yield from
,仍是來看看官方給的例子:編程
def accumulate():
tally = 0
while 1:
next = yield
if next is None:
return tally
tally += next
def gather_tallies(tallies):
while 1:
tally = yield from accumulate()
tallies.append(tally)
tallies = []
acc = gather_tallies(tallies)
next(acc) # Ensure the accumulator is ready to accept values
for i in range(4):
acc.send(i)
acc.send(None) # Finish the first tally
for i in range(5):
acc.send(i)
acc.send(None) # Finish the second tally
print(tallies)
複製代碼
我還專門爲此錄製了一段視頻,你能夠配合文字一塊兒看,或者你也能夠打開 pycharm 以及任何調試工具,本身調試一下。 視頻連接瀏覽器
來一塊兒 break down:bash
從acc = gather_tallies(tallies)
這一行開始,因爲gather_tallies
函數中有一個 yield,因此不會while 1
當即執行(你從視頻中能夠看到,acc 是一個 generator 類型)。微信
next(acc)
:網絡
next()會運行到下一個 yield,或者報StopIteration錯誤。
next(acc)
進入到函數體gather_tallies,gather_tallies中有一個yield from accumulate()
,next(acc)不會在這一處停,而是進入到『subgenerator』accumulate裏面,而後在next = yield
處,遇到了yield
,而後暫停函數,返回。
for i in range(4):
acc.send(i)
複製代碼
理解一下 acc.send(value)
有什麼用:
xxx = yield
中的xxx
,這個例子中就是next
。accumulate
函數中的那個while 循環,經過判斷next
的值是否是 None 來決定要不要退出循環。在for i in range(4)
這個for循環裏面,i 都不爲 None,因此 while 循環沒有斷。可是,根據咱們前面講的:next()會運行到下一個 yield的地方停下來,這個 while 循環一圈,又再次遇到了yield
,因此他會暫停這個函數,把控制權交還給主線程。
理清一下:對於accumulate來講,他的死循環是沒有結束的,下一次經過 next()恢復他運行時,他仍是在運行他的死循環。對於gather_tallies來講,他的
yield from accumulate()
也還沒運行完。對於整個程序來講,確實在主進程和accumulate函數體之間進行了屢次跳轉。
接下來看第一個acc.send(None)
:這時next
變量的值變成了None
,if next is None
條件成立,而後返回tally
給上一層函數。(計算一下,tally 的值爲0 + 1 + 2 + 3 = 6)。這個返回值就賦值給了gather_tallies
中的gally
。這裏須要注意的是,gather_tallies
的死循環還沒結束,因此此時調用next(acc)
不會報StopIteration
錯誤。
for i in range(5):
acc.send(i)
acc.send(None) # Finish the second tally
複製代碼
這一部分和前面的邏輯是同樣的。acc.send(i)會先進入gather_tallies
,而後進入accumulate
,把值賦給next
。acc.send(None)
中止循環。最後tally的值爲10(0 + 1 + 2 + 3 + 4)。
最終tallies列表爲:[6,10]
。
看一下 wikipedia 上 Coroutine的定義:
Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed.
關鍵點在於by allowing execution to be suspended and resumed.(讓執行能夠被暫停和被恢復)。通俗點說,就是:
coroutines are functions whose execution you can pause。(來自How the heck does async/await work in Python 3.5?)
這不就是生成器嗎?
Python生成器的概念最先起源於 python2.2(2001年)時剔除的 pep255,受Icon 編程語言啓發。
生成器有一個好處,不浪費空間,看下面這個例子:
def eager_range(up_to):
"""Create a list of integers, from 0 to up_to, exclusive."""
sequence = []
index = 0
while index < up_to:
sequence.append(index)
index += 1
return sequence
複製代碼
若是用這個函數生成一個10W 長度的列表,須要等待 while 循環運行結束返回。而後這個sequence
列表將會佔據10W 個元素的空間。耗時不說(從可以第一次可以使用到 sequence 列表的時間這個角度來看),佔用空間還很大。
藉助上一部分講的yield
,稍做修改:
def lazy_range(up_to):
"""Generator to return the sequence of integers from 0 to up_to, exclusive."""
index = 0
while index < up_to:
yield index
index += 1
複製代碼
這樣就只須要佔據一個元素的空間了,並且當即就能夠用到 range,不須要等他所有生成完。
一些有先見之明的前輩想到,若是咱們可以利用生成器可以暫停的這一特性,而後想辦法添加 send stuff back 的功能,這不就符合維基百科對於協程的定義了麼?
因而就有了pep342。
pep342中提到了一個send()
方法,容許咱們把一個"stuff"送回生成器裏面,讓他接着運行。來看下面這個例子:
def jumping_range(up_to):
"""Generator for the sequence of integers from 0 to up_to, exclusive. Sending a value into the generator will shift the sequence by that amount. """
index = 0
while index < up_to:
jump = yield index
if jump is None:
jump = 1
index += jump
if __name__ == '__main__':
iterator = jumping_range(5)
print(next(iterator)) # 0
print(iterator.send(2)) # 2
print(next(iterator)) # 3
print(iterator.send(-1)) # 2
for x in iterator:
print(x) # 3, 4
複製代碼
這裏的send
把一個『stuff』送進去給生成器,賦值給 jump,而後判斷jump 是否是 None,來執行對應的邏輯。
自從Python2.5以後,關於生成器就沒作什麼大的改進了,直到 Python3.3時提出的pep380。這個 pep 提案提出了yield from
這個能夠理解爲語法糖的東西,使得編寫生成器更加簡潔:
def lazy_range(up_to):
"""Generator to return the sequence of integers from 0 to up_to, exclusive."""
index = 0
def gratuitous_refactor():
nonlocal index
while index < up_to:
yield index
index += 1
yield from gratuitous_refactor()
複製代碼
第一節咱們已經詳細講過 yield from 了,這裏就不贅述了。
若是你有 js 編程經驗,確定對事件循環有所瞭解。
理解一個概念,最好也是最有bigger的就是翻出 wikipedia:
an event loop "is a programming construct that waits for and dispatches events or messages in a program" - 來源於Event loop - wikipedia
簡單來講,eventloop 實現當 A 事件發生時,作 B 操做。拿瀏覽器中的JavaScript事件循環來講,你點擊了某個東西(A 事件發生了),就會觸發定義好了的onclick
函數(作 B 操做)。
在 Python 中,asyncio 提供了一個 eventloop(回顧一下上一篇的例子),asyncio 主要聚焦的是網絡請求領域,這裏的『A 事件發生』主要就是 socket 能夠寫、 socket能夠讀(經過selectors
模塊)。
到這個時期,Python 已經經過Concurrent programming
的形式具有了異步編程的實力了。
Concurrent programming
只在一個 thread 裏面執行。go 語言blog 中有一個很是不錯的視頻:Concurrency is not parallelism,很值得一看。
這個時期的asyncio代碼是這樣的:
import asyncio
# Borrowed from http://curio.readthedocs.org/en/latest/tutorial.html.
@asyncio.coroutine
def countdown(number, n):
while n > 0:
print('T-minus', n, '({})'.format(number))
yield from asyncio.sleep(1)
n -= 1
loop = asyncio.get_event_loop()
tasks = [
asyncio.ensure_future(countdown("A", 2)),
asyncio.ensure_future(countdown("B", 3))]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
複製代碼
輸出結果爲:
T-minus 2 (A)
T-minus 3 (B)
T-minus 1 (A)
T-minus 2 (B)
T-minus 1 (B)
複製代碼
這時使用的是asyncio.coroutine
修飾器,用來標記某個函數能夠被 asyncio 的事件循環使用。
看到yield from asyncio.sleep(1)
了嗎?經過對一個asyncio.Future object yield from
,就把這個future object 交給了事件循環,當這個 object 在等待某件事情發生時(這個例子中就是等待 asyncio.sleep(1),等待 1s 事後),把函數暫停,開始作其餘的事情。當這個future object 等待的事情發生時,事件循環就會注意到,而後經過調用send()
方法,讓它從上次暫停的地方恢復運行。
break down 一下上面這個代碼:
事件循環開啓了兩個countdown()
協程調用,一直運行到yield from asyncio.sleep(1)
,這會返回一個 future object,而後暫停,接下來事件循環會一直監視這兩個future object。1秒事後,事件循環就會把 future object send()給coroutine,coroutine又會接着運行,打印出T-minus 2 (A)
等。
python3.4的
@asyncio.coroutine
def py34_coro():
yield from stuff()
複製代碼
到了 Python3.5,能夠用一種更加簡潔的語法表示:
async def py35_coro():
await stuff()
複製代碼
這種變化,從語法上面來說並沒什麼特別大的區別。真正重要的是,是協程在 Python 中哲學地位的提升。 在 python3.4及以前,異步函數更多就是一種很普通的標記(修飾器),在此以後,協程變成了一種基本的抽象基礎類型(abstract base class):class collections.abc.Coroutine。
How the heck does async/await work in Python 3.5?一文中還講到了async
、await
底層 bytecode 的實現,這裏就不深刻了,畢竟篇幅有限。
Python 核心開發者(也是我最喜歡的 pycon talker 之一)David M. Beazley在PyCon Brasil 2015的這一個演講中提到:咱們應該把 async
和await
看做是API,而不是實現。 也就是說,async
、await
不等於asyncio
,asyncio
只不過是async
、await
的一種實現。(固然是asyncio
使得異步編程在 Python3.4中成爲可能,從而推進了async
、await
的出現)
他還開源了一個項目github.com/dabeaz/curi…,底層的事件循環機制和 asyncio
不同,asyncio
使用的是future object
,curio
使用的是tuple
。同時,這兩個 library 有不一樣的專一點,asyncio
是一整套的框架,curio
則相對更加輕量級,用戶本身須要考慮到事情更多。
How the heck does async/await work in Python 3.5?此文還有一個簡單的事件循環實現例子,有興趣能夠看一下,後面有時間的話也許會一塊兒實現一下。
最重要的一點感覺是:Nothing is Magic。如今你應該可以對 Python 的協程有了在總體上有了一個把握。
若是你像我同樣真正熱愛計算機科學,喜歡研究底層邏輯,歡迎關注個人微信公衆號: