本文參考了:
yield 和 yield from
先讓我們來學習或者回顧一下yield
和yield from
的用法。如果你很自信自己完成理解了,可以跳到下一部分。
Python3.3提出了一種新的語法:yield from
。
yield from iterator
複製程式碼
本質上也就相當於:
for x in iterator:
yield x
複製程式碼
下面的這個例子中,兩個 yield from
加起來,就組合得到了一個大的iterable
(例子來源於官網3.3 release):
>>> 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:
從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)
有什麼用:
- 第一步:回到上一次暫停的地方
- 第二步:把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]
。
Python async await發展簡史
看一下 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?)
這不就是生成器嗎?
python2.2 - 生成器起源
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,不需要等他全部生成完。
python2.5 : send stuff back
一些有先見之明的前輩想到,如果我們能夠利用生成器能夠暫停的這一特性,然後想辦法新增 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,來執行對應的邏輯。
python3.3 yield from
自從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 了,這裡就不贅述了。
python3.4 asyncio模組
插播:事件迴圈(eventloop)
如果你有 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 程式碼
這個時期的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.5 async await
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 的實現,這裡就不深入了,畢竟篇幅有限。
把 async、await看作是API 而不是 implementation
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?此文還有一個簡單的事件迴圈實現例子,有興趣可以看一下,後面有時間的話也許會一起實現一下。
總結一下
- 協程只有一個 thread。
- 作業系統排程程式、協程用事件迴圈排程函式。
- async、await 把協程在 Python 中的哲學地位提高了一個檔次。
最重要的一點感受是:Nothing is Magic。現在你應該能夠對 Python 的協程有了在整體上有了一個把握。
如果你像我一樣真正熱愛電腦科學,喜歡研究底層邏輯,歡迎關注我的微信公眾號: