原文標題:PEP 0492 -- Coroutines with async and await syntax
原文連接:https://www.python.org/dev/peps/pep-0492/
生效於:Python 3.5
翻譯參照版本:05-May-2015
翻譯最後修改:2015年8月22日
翻譯出處:http://www.cnblogs.com/animalize/p/4738941.htmlhtml
用幾句話說明這個PEP:python
- 把協程的概念從生成器獨立出來,併爲之添加了新語句(async/await)。
- 可是在CPython的內部實現,協程仍然是一個生成器。
- 增長了異步迭代器(async for),異步迭代器的__aiter__、__anext__函數是協程,能夠將程序掛起。
- 增長了異步上下文管理器(async with),異步上下文管理器的__aenter__、__aexit__函數是協程,能夠將程序掛起。
不斷增多的Internet鏈接程序刺激了對響應性、伸縮性代碼的需求。這個PEP的目標在於:制訂顯式的異步/併發語法,比傳統的Python方法更易用、更豐富。程序員
咱們準備把協程(協同程序)的概念獨立出來,併爲其使用新的語法。最終目標是創建一個通用、易學的異步編程的構思模型,並儘可能與同步編程的風格類似。數據庫
這個PEP假設異步任務被一個事件循環器(相似於標準庫裏的 asyncio.events.AbstractEventLoop)管理和調度。然而咱們並不會依賴某個事件循環器的具體實現方法,從本質上說只與此相關:採用yield做爲給調度器的信號,表示協程將會掛起、等待一個異步事件(如IO)的完成。編程
在這個異步編程不斷增加的時期,咱們相信這些改變將會使Python保持必定的競爭性,就像許多其它編程語言已經、將要進行的改變那樣。api
根據Python 3.5 Beta期間的反饋,進行了從新設計,明確地把協程從生成器裏獨立出來了。協程如今是原生的,有明確的獨立類型,而不是做爲生成器的一種特殊形式。緩存
這個改變,主要是爲了解決在Tornado裏使用協程出現的一些問題。
【譯註:在Tornado 4.3已經可使用新的async/await語句,詳見此連接】session
在之前,咱們能夠用生成器實現協程(PEP 342),後來又對其進行了改進,引入了yield from語法(PEP 380)。但仍有一些缺點:併發
這個PEP把協程從生成器獨立出來,成爲Python的一個原生事物。這會消除協程和生成器之間的混淆,方便編寫不依賴特定庫的協程代碼。也爲linter和IDE進行代碼靜態分析提供了機會。
【譯註:在CPython內部,原生協程仍然是基於生成器實現的。】app
使用原生協程和相應的新語法,咱們能夠在異步編程時使用上下文管理器(context manager)和迭代器。以下文所示,新的async with語句能夠在進入、離開運行上下文(runtime context)時進行異步調用,而async for語句能夠在迭代時進行異步調用。
請理解Python現有的協程(見PEP 342和PEP 380),此次改變的動機來自於asyncio框架(PEP 3156)和Confunctions提案(PEP 3152,此PEP已經被廢棄)。
由此,在本文中,咱們使用「原生協程」指用新語法聲明的協程。「生成器實現的協程」指用傳統方法實現的協程。「協程」則用在兩個均可以使用的地方。
使用如下語法聲明原生協程:
async def read_data(db): pass
協程語法的關鍵點:
types模塊添加了一個新函數coroutine(fn),使用它,「生成器實現的協程」和「原生協程」之間能夠進行互操做。
【譯註:這是個裝飾器,能把現有代碼的「用生成器實現的協程」轉化爲與「原生協程」兼容的形式】
@types.coroutine def process_data(db): data = yield from read_data(db) ...
coroutine(fn)函數給生成器的代碼對象(code object)設置CO_ITERABLE_COROUTINE標識,使它返回一個協程對象。
若是fn不是一個生成器函數,它什麼也不作。若是fn是一個生成器函數,則會被一個awaitable代理對象(proxy object)包裝(wrapped),詳見下文的「定義awaitable對象」。
注意, types.coroutine()不會設置CO_COROUTINE標識,只有用新語法定義的原生協程纔會有這個標識。
【譯註: @types.coroutine裝飾器僅給生成器函數設置一個CO_ITERABLE_COROUTINE標識,除此以外什麼也不作。可是若是生成器函數沒有這個標識,await語句不會接受它的對象做爲參數。】
新的await表達式用於得到協程執行的結果:
async def read_data(db): data = await db.fetch('SELECT ...') ...
await和yield from相似,它掛起read_data的執行,直到db.fetch執行完畢並返回結果。
以CPython內部,await使用了yield from的實現,但加入了一個額外步驟——驗證它的參數類型。await只接受awaitable對象,awaitable對象是如下的其中一個:
一個有__await__方法的對象(__await__方法返回的一個迭代器)
每一個yield from調用鏈條都會追溯到一個最終的yield語句,這是Future實現的基本機制。在Python內部,因爲協程是生成器的一種特殊形式,因此每一個await最終會被await調用鏈條上的某個yield語句掛起。(詳情請參考PEP 3156)
【譯註:Future對象用來表示在將來完成的某項任務。】
爲了讓協程也有這樣的行爲,添加了一個新的魔術方法__await__。【譯註:一系列遞歸調用必終結於某個return具體結果的語句;一個yield from調用鏈條必終結於某個yield語句;相似的,一個await調用鏈條必終結於某個有__await__方法的對象。】例如,在asyncio模塊,要想在await語句裏使用Future對象,惟一的修改是給asyncio.Future加一行:__await__ = __iter__
在本文中,有__await__方法的對象被稱爲Future-like對象。
【譯註:協程會被await語句掛起,直到await語句右邊的Future-like對象的__await__執行完畢、返回結果。】
另外,請注意__aiter__方法(見下文)不能被用於此目的。那是另外一套東西,這樣作的話,相似於callable對象使用__iter__代替__call__。【譯註:意思是__await__和__aiter__的關係有點像callable對象裏__call__和__iter__的關係】
若是__await__返回的不是一個迭代器,則引起TypeError異常。在CPython C API,有tp_as_async.am_await函數的對象,該函數返回一個迭代器(相似__await__方法)
若是在async def函數以外使用await語句,會引起SyntaxError異常。這和在def函數以外使用yield語句同樣。
若是await右邊不是一個awaitable對象,會引起TypeError異常。
【譯註:整體略去不譯。】
await語句和yield、yield from的一個區別是:await語句多數狀況下不須要被圓括號包圍。
有效用法:
表達式 | 被解析爲 |
---|---|
if await fut: pass | if (await fut): pass |
if await fut + 1: pass | if (await fut) + 1: pass |
pair = await fut, 'spam' | pair = (await fut), 'spam' |
with await fut, open(): pass | with (await fut), open(): pass |
await foo()['spam'].baz()() | await ( foo()['spam'].baz()() ) |
return await coro() | return ( await coro() ) |
res = await coro() ** 2 | res = (await coro()) ** 2 |
func(a1=await coro(), a2=0) | func(a1=(await coro()), a2=0) |
await foo() + await bar() | (await foo()) + (await bar()) |
-await foo() | -(await foo()) |
無效用法:
表達式 | 應該寫爲 |
---|---|
await await coro() | await (await coro()) |
await -coro() | await (-coro()) |
異步上下文管理器(asynchronous context manager),能夠在它的enter和exit方法裏掛起、調用異步代碼。
爲此,咱們設計了一套方案,添加了兩個新的魔術方法:__aenter__和__aexit__,它們必須返回一個awaitable。
異步上下文管理器的一個示例:
class AsyncContextManager: async def __aenter__(self): await log('entering context') async def __aexit__(self, exc_type, exc, tb): await log('exiting context')
採納了一個異步上下文管理器的新語法:
async with EXPR as VAR: BLOCK
在語義上等同於:
mgr = (EXPR) aexit = type(mgr).__aexit__ aenter = type(mgr).__aenter__(mgr) exc = True VAR = await aenter try: BLOCK except: if not await aexit(mgr, *sys.exc_info()): raise else: await aexit(mgr, None, None, None)
和普通的with語句同樣,能夠在單個async with語句裏指定多個上下文管理器。
在使用async with時,若是上下文管理器沒有__aenter__和__aexit__方法,則會引起錯誤。在async def函數以外使用async with則會引起SyntaxError異常。
有了異步上下文管理器,協程很容易實現對數據庫處理的恰當管理。
async def commit(session, data): ... async with session.transaction(): ... await session.update(data) ...
再好比,加鎖時看着更簡潔:
async with lock: ...
而不是:
with (yield from lock): ...
異步迭代器能夠在它的iter實現裏掛起、調用異步代碼,也能夠在它的__next__方法裏掛起、調用異步代碼。要支持異步迭代,須要:
異步迭代的一個示例:
class AsyncIterable: async def __aiter__(self): return self async def __anext__(self): data = await self.fetch_data() if data: return data else: raise StopAsyncIteration async def fetch_data(self): ...
採納了一個迭代異步迭代器的新語法:
async for TARGET in ITER: BLOCK else: BLOCK2
在語義上等同於:
iter = (ITER) iter = await type(iter).__aiter__(iter) running = True while running: try: TARGET = await type(iter).__anext__(iter) except StopAsyncIteration: running = False else: BLOCK else: BLOCK2
若是async for的迭代器不支持__aiter__方法,則引起TypeError異常。若是在async def函數外使用async for,則引起SyntaxError異常。
和普通的for語句同樣,async for有一個可選的else分句。
有了異步迭代,咱們能夠在迭代時異步緩衝(buffer)數據:
async for data in cursor: ...
Cursor是一個異步迭代器,能夠從數據庫預讀4行數據並緩存。見如下代碼:
# 【譯註:此代碼已被修改,望更易理解】 class Cursor: def __init__(self): self.buffer = collections.deque() async def _prefetch(self): row1, row2, row3, row4 = await fetch_from_db() self.buffer.append(row1) self.buffer.append(row2) self.buffer.append(row3) self.buffer.append(row4) async def __aiter__(self): return self async def __anext__(self): if not self.buffer: self.buffer = await self._prefetch() if not self.buffer: raise StopAsyncIteration return self.buffer.popleft()
而後,能夠這樣使用Cursor類:
async for row in Cursor(): print(row)
與下述代碼相同:
i = await Cursor().__aiter__() while True: try: row = await i.__anext__() except StopAsyncIteration: break else: print(row)
這是一個便利類,用於把普通的迭代對象轉變爲一個異步迭代對象。雖然這個類沒什麼實際用處,但它演示了普通迭代器和異步迭代器的關係:
class AsyncIteratorWrapper: def __init__(self, obj): self._it = iter(obj) async def __aiter__(self): return self async def __anext__(self): try: value = next(self._it) except StopIteration: raise StopAsyncIteration return value async for letter in AsyncIteratorWrapper("abc"): print(letter)
在CPython內部,協程的實現仍然是基於生成器的。因此,在PEP 479生效以前【譯註:將在Python 3.7正式生效,在3.五、3.6須要from __future__ import generator_stop】,如下兩個代碼是徹底同樣的,最終都是給外部代碼拋出一個StopIteration('spam')異常:
def g1(): yield from fut return 'spam'
和
def g2(): yield from fut raise StopIteration('spam')
因爲PEP 479已被正式採納,並做用於協程,如下代碼的StopIteration會被包裝(wrapp)成一個RuntimeError。
async def a1(): await fut raise StopIteration('spam')
因此,要想通知外部代碼迭代已經結束,拋出一個StopIteration異常的方法不行了。所以,添加了一個新的內置異常StopAsyncIteration,用於表示迭代結束。
此外,根據PEP 479,協程拋出的全部StopIteration異常都會被包裝成RuntimeError異常。
【譯註:若是函數生成器內部的代碼出現StopIteration異常、且未被捕獲,則外部代碼會誤認爲生成器已經迭代結束。爲了消除這樣的誤會,PEP 479的規定,Python會把生成器內部拋出的StopIteration包裝成RuntimeError。
在之後,若是想主動結束一個函數生成器的迭代,用return語句便可(這時函數生成器仍然會給外部代碼拋出一個StopIteration異常),而不是之前的使用raise StopIteration語句(這樣的話,StopIteration會被包裝成一個RuntimeError)。】
這一小節只對原生協程有效(用async def語法定義的、有CO_COROUTINE標識的)。對於asyncio模塊裏現有的「基於生成器的協程」,仍然保持不變。
爲了在概念上把協程和生成器區分開來,作了如下規定:
【譯註: @asyncio.coroutine裝飾器,在Python 3.4,用於把一個函數裝飾爲一個協程。有些函數並非生成器函數(不含yield或yield from語句),也能夠用 @asyncio.coroutine裝飾爲一個協程。
在Python 3.5中, @asyncio.coroutine也會有 @types.coroutine的效果——使函數的對象能夠被await語句接受。】
在CPython內部,協程是基於生成器實現的,所以它們有共同的代碼。像生成器對象那樣,協程也有throw(),send()和close()方法。
對於協程,StopIteration和GeneratorExit起着一樣的做用(雖然PEP 479已經應用於協程)。詳見PEP 34二、PEP 380,以及Python文檔。
對於協程,send(),throw()方法用於往Future-like對象發送內容、拋出異常。
新手在使用協程時可能忘記使用yield from語句,好比:
@asyncio.coroutine def useful(): asyncio.sleep(1) # 前面忘寫yield from,因此程序在這裏不會掛起1秒
在asyncio裏,對於此類錯誤,有一個特定的調試方法。裝飾器 @coroutine用一個特定的對象包裝(wrap)全部函數,這個對象有一個析構函數(destructor)用於記錄警告信息。不管什麼時候,一旦被裝飾過的生成器被垃圾回收,會生成一個詳細的記錄信息(具體哪一個函數、回收時的stack trace等等)。包裝對象提供一個__repr__方法用來輸出關於生成器的詳細信息。
惟一的問題是如何啓用這些調試工具,因爲這些調試工具在生產模式裏什麼也不作,好比 @coroutine必須是在系統變量PYTHONASYNCIODEBUG出現時才具備調試功能。這時能夠給asyncio程序進行以下設置:EventLoop.set_debug(true),這時使用另外一套調試工具,對 @coroutine的行爲沒有影響。
根據本文,協程是原生的,已經在概念上和生成器進行了區分。一個從未await的協程會拋出一個RuntimeWarning,除此以外,給sys模塊增長了兩個新函數set_coroutine_wrapper和get_coroutine_wrapper,它們會爲asyncio和其它框架啓用高級調試工具,好比顯示協程在何處被建立、協程在何處被垃圾回收的詳細stack trace。
爲了能更好的與現有框架(如Tornado)和其它編譯器(如Cython)相整合,增長了兩個新的抽象基類(Abstract Base Classes):
注意,「基於生成器的協程」(有CO_ITERABLE_COROUTINE標識)並不實現__await__方法,所以它們不是collections.abc.Coroutine和collections.abc.Awaitable的實例:
@types.coroutine def gencoro(): yield assert not isinstance(gencoro(), collections.abc.Coroutine) # however: assert inspect.isawaitable(gencoro())
爲了更容易地對異步迭代進行調試,又增長了兩個抽象基類:
原生協程函數 Native coroutine function
由async def定義的協程函數,可使用await和return value語句。見「新的協程聲明語法」一節。
原生協程 Native coroutine
原生協程函數返回的對象。見「await表達式」一節。
基於生成器的協程函數 Generator-based coroutine function
基於生成器語法的協程,最多見的是用 @asyncio.coroutine裝飾過的函數。
基於生成器的協程 Generator-based coroutine
基於生成器的協程函數返回的對象。
協程 Coroutine
「原生協程」和「基於生成器的協程」都是協程。
協程對象 Coroutine object
「原生協程對象」和「基於生成器的協程對象」都是協程對象。
Future-like對象 Future-like object
一個有__await__方法的對象,或一個有tp_as_async->am_await函數的C語言對象,它們返回一個迭代器。Future-like對象能夠在協程裏被一條await語句消費(consume)。協程會被await語句掛起,直到await語句右邊的Future-like對象的__await__執行完畢、返回結果。見「await表達式」一節。
Awaitable
一個Future-like對象或一個協程對象。見「await表達式」一節。
異步上下文管理器 Asynchronous context manager
有__aenter__和__aexit__方法的對象,能夠被async with語句使用。見「異步上下文管理器和‘async with’」一節。
可異步迭代對象 Asynchronous iterable
有__aiter__方法的對象, 該方法返回一個異步迭代器對象。能夠被async for語句使用。見「異步迭代器和‘async for’」一節。
異步迭代器 Asynchronous iterator
有__anext__方法的對象。見「異步迭代器和‘async for’」一節。
【譯註:感受餘下大部份內容沒必要翻譯,若有須要請參看原文。這裏只挑選部份內容翻譯。】
本PEP保持100%向後兼容。
asyncio模塊已經可使用新語法,並通過測試,100%與async/await兼容。現有的使用asyncio的代碼在使用新語法時能夠保持不變。
爲此,對asyncio模塊主要作了以下修改:
因爲未經裝飾的生成器不能yield from原生協程對象(詳見「和生成器的不一樣之處」一節),所以在使用新語法前,請確保全部「基於生成器的協程」都被 @asyncio.coroutine裝飾器裝飾。
async和await在CPython 3.五、3.6裏暫時不是正式的關鍵字,在CPython 3.7它們將變成正式的關鍵字。若是不這樣,恐怕對現有代碼的遷移形成困難。 【譯註:在某些現有代碼裏,可能使用了async和await做爲變量名/函數名。然而Python不容許把關鍵字看成變量名/函數名,因此3.五、3.6給程序員留了一些遷移時間。】