PEP 492 部分翻譯

摘要:

  1. 針對PEP 492的部分內容進行了翻譯
  2. 主要講述了 Python 的async, await關鍵字

規範

這個提案介紹了新的語法和語義來增強Python 對協程的支持。這個規範假設你擁有 Python 協程實現的相關知識(PEP 342 和 PEP 380)。這個語法改變的動機是在 asyncio 框架(PEP 3156)和 協作函數 提案(PEP 3152,爲了有利於本規範現在被拒絕了)。

從文檔這一點我們使用 原生協程 的關鍵字去索引使用新語法生成的函數。基於生成器的協程 關鍵字用來描述基於生成器語法生成的協程。協程 關鍵字根據上下文,在兩種語義中都適用。

新的生成器聲明語法

如下的新語法用來去生成一個新的 原生協程 :

async def read_data(db):
    pass

協程的關鍵屬性是:

  • async def 定義的函數總是協程,甚至它們不包含 await 表達式
  • async函數中有yield或者yield from關鍵字會拋出SyntaxError
  • 在內部,兩個新的代碼對象標記被使用:
    • CO_COROUTINE 用來去標記 原生協程 (使用新語法定義的)
    • CO_ITERABLE_COROUTINE 用來去生成 基於生成器的協程原生協程兼容 (由 types.coroutine() 函數來設定)
  • 通常生成器在被調用的時候返回一個 生成器對象,相似的,協程返回一個 協程 對象。
  • StopIteration異常不會在協程外傳播,而是替換成了 RuntimeError。普通的生成器如果要這種行爲需要一個 future import (參考 PEP 479)
  • 當一個協程被GC回收時,如果它沒有被await 的話它將會拋出一個 RuntimeWaring。(參考 Debugging Features)
  • 這部分內容也可以在Coroutine objects部分看到

types.coroutine()

這是一個在 types 模塊中新添加的方法coroutine(fn)。它允許在已經存在的異步IO中 基於生成器的協程 和 本PEP介紹的 原生協程 之間進行交互。

@types.coroutine
def process_data(db):
    data = yield from read_data(db)
    ...

這個函數將 CO_ITERABLE_COROUTINE 標誌應用到生成器函數代碼對象上,使它返回一個協程對象。

如果參數fn不是一個生成器函數的話,它將會將它封裝。如果它返回了一個生成器,它將會將它封裝成爲一個 awaitable 的代理對象(參見下面 awaitable 對象的定義)。

注意,CO_COROUTINE 標記沒有被應用到types.coroutine()上,這使得從基於生成器協程中分離出一個使用新語法定義的原生協程成爲可能。

Await 表達式

如下的新的await表達式用來獲取一個協程執行的結果。

async def read_data(db):
    data = await db.fetch('SELECT ...')
    ...

await 相似於 yield from,暫停 read_data 協程的執行,直到db.fetch()這個 awaitable 對象完成並且返回數據。

它使用了 yield from 的實現,並添加上了額外的驗證參數的步驟。await 僅僅接收一個 awaitable 對象,它可以是如下幾種形式:

  • 從一個 原生協程函數 返回的 原生協程 對象。
  • 一個基於生成器協程 對象,並且使用了 types.coroutine() 修飾符。
  • 一個對象擁有 __await__ 方法並且返回了一個迭代器。

任何一個 yield from 鏈都會以一個 yield 來結束。這是一個基本的 Futures 實現的機制。因爲,內部來說,協程是一種特殊類型的生成器。每個await都被await調用鏈下一級的某個yield給暫停了(請參考 PEP 3156 查看更多的細節解釋)。
去爲協程開啓這個特性,一個叫做 __await__ 的新的魔法方法被添加了。在異步IO中,例如,去在 await 語句中開啓 Future 對象,僅僅需要做的改變就是爲asyncic.Future類添加一行 __await__ = __iter__
在本 PEP 的剩餘部分,擁有__await__ 的對象就被叫做 Future-like 對象。
如果 __await__ 方法返回的不是一個迭代器,將會拋出一個TypeError
+ 使用CPython C API 函數tp_as_async.am_await定義的對象將會返回一個迭代器(類似於__await__方法)

如果在async def函數外使用await語句將總會拋出一個語法錯誤(就像在def外部使用yield語句)。

如果在await語句後跟的不是一個awaitable對象,將會拋出一個TypeError

更新操作符優先級表

await關鍵字是被如下定義的:

power ::= await ["**" u_expr]
await ::= ["await"] primary

primay 代表了語言中最緊緊綁定的操作符,它的語法是:

primary ::= atom | attributeref | subscription | slicing | call

參見 Python 文檔 Primaries 查看更多細節

關鍵字await不同於yieldyield from操作符,await表達式大部分時間不需要在他們周圍加上括號。

同樣的,yield from允許一個表達式作爲它的參數,包括表達式像yield from a() + b(),將會被解析成yield from (a() + b()),這通常都是一個BUG。一般來說,任何算數運算的結果都不是一個awaitable的對象。爲了避免這種類型的錯誤,我們決定讓await[]().有更低的優先級,但是比**操作符的優先級要高。

Operator Description
yield x , yield from x Yield expression
lambda Lambda expression
if – else Conditional expression
or Boolean OR
and Boolean AND
not x Boolean NOT
in , not in , is , is not , < , <= , > , >= , != , == Comparisons, including membership tests and identity tests
| Bitwise OR
^ Bitwise XOR
& Bitwise AND
<< , >> Shifts
    , -
Addition and subtraction
    , @ , / , // , %
Multiplication, matrix multiplication, division, remainder
+x , -x , ~x Positive, negative, bitwise NOT
** Exponentiation
await x Await expression
x[index] , x[index:index] , x(arguments…) , x.attribute Subscription, slicing, call, attribute reference
(expressions…) , [expressions…] , {key: value…} , {expressions…} Binding or tuple display, list display, dictionary display, set display

“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())

異步上下文管理器和 “async with”

一個異步的上下文管理器是一種特殊的上下文管理器,能夠在它的 enter 和 exit 方法中暫停。

爲了使這種設想成爲可能,一種新的協議用來支持異步的上下文管理器被提出了。兩個魔法方法被添加了,__aenter____aexit__。這兩個都必須返回一個awaitable的對象。

一個異步的上下文管理器的例子是:

class AsyncContextManager:
    async def __aenter__(self):
        await log('enter 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)
exec = True

VAR = await aenter

try:
    block
except:
    if not await(mgr, *sys.exc_info()):
        raise
else:
    await aexit(mgr, None, None, None)

就像普通的with語句一樣,async with語句也能夠指定多個上下文管理器。

將一個普通的沒有__aenter____aexit__方法的上下文管理器傳遞給async with語句是錯誤的。同時,在async def函數外部使用async with語句也是一個語法錯誤。

例子

使用異步的上下文管理器很容易去正確實現協程的數據庫事物管理:

async def commit(session, data):
    ...
    async with sesson.transaction():
        ...
        await session.update(data)
        ...

同時,代碼需要看起來更輕量級一些:

async with lock:
    ...

而不是:

with (yield from lock):
    ...

異步迭代器和 “async for”

一個異步可迭代對象能夠在它的迭代器實現中異步地調用代碼,異步迭代器能夠在它的next方法中調用異步代碼,爲了支持異步迭代:

  1. 一個對象必須實現一個__aiter__方法(或者使用了 CPython 的C API 的tp_as_async.am_aiter接口),返回一個異步迭代器對象。
  2. 一個異步迭代器對象必須實現一個__anext__方法(或者也可以使用 CPython 的 C API 的tp_as_async.am_next接口),返回一個awaitable對象。
  3. 爲了停止迭代,__anext__必須拋出一個StopAsyncIteration異常。

一個異步迭代的例子如下所示:

class AsyncIterable:
    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 = type(iter).__aiter__(iter)
running = True
while running:
    try:
        TARGET = await type(iter).__anext__(iter)
    except StopAsyncIteration:
        running = False
    else:
        BLOCK

async for語句拋出一個沒有__aiter__方法的普通迭代器將會拋出一個TypeError。在async for外部使用async for語句將會拋出一個SyntaxError

就像普通的for語句一樣,async for有可選的else子句。

例子1

使用異步迭代協議可以在迭代期間異步緩存數據:

async for data in cursor:
    ...

cursor是一個異步迭代器,在N次迭代後從數據庫中取出N行的數據。

如下的代碼說明了一種新的異步迭代協議:

class Cursor:
    def __init__(self):
        self.buffer = collections.deque()

    async def _prefetch(self):
        ...

    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 = Cursor().__aiter__()
while True:
    try:
        row = await i.__anext__()
    except StopAsyncIteration:
        break
    else:
        print(row)

例子2

如下的例子是一個使用類將普通迭代器轉換成異步的。雖然這並不是一個非常有用的事情,但是如下的代碼說明了普通迭代器和異步迭代器之間的關係:

class AsyncIteratorWrapper:
    def __init__(self, obj):
        self._it = iter(obj)

    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)

爲什麼是StopAsyncIteration異常

協程對象內部仍然是基於生成器的。所以,在PEP 479之前,在下面這兩種代碼中沒有基本的區別:

def g1():
    yield from fut
    return 'spam'
def g2():
    yield from fut
    raise StopIteration('spam')

因爲 PEP 479 被接受了,而且默認在協程對象中開啓,所以如下的例子將會把它的StopIteration包裹在一個RuntimeError中:

async def a1():
    await fut
    raise StopIteration('spam')

唯一的方式就是由迭代器告訴外部的代碼,迭代結束了而不能使用StopIteration異常。因此,一個新的內建的異常類StopAsyncIteration 被添加了。

此外,由PEP 479的語義可得,所有協程中的StopIteration將會由一個RuntimeError來包裹。

發佈了69 篇原創文章 · 獲贊 17 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章