В Python 3.5 була додана можливість асинхронного програмування з використанням async&await. Тепер у пітон пропонують додати асинхронні генератори.
Звичайні генератори (представлені в PEP 255) дозволили елегантно писати функції, що покроково обробляли дані та вели себе як ітератори.
Але зараз немає аналогічної концепції для асинхронного for (async for
). Саме таку концепцію було запропоновано в PEP 525. Зазначається, що саме такий підхід забезпечує подвійний приріст продуктивності в порівнянні з використанням асинхронних ітераторів.
Ось так, наприклад, можна написати ітератор, що повертає числа з заданим інтервалом:
class Ticker:
"""Повертає числа від 0 до `to` кожні `delay` секунд."""
def __init__(self, delay, to):
self.delay = delay
self.i = 0
self.to = to
def __aiter__(self):
return self
async def __anext__(self):
i = self.i
if i >= self.to:
raise StopAsyncIteration
self.i += 1
if i:
await asyncio.sleep(self.delay)
return i
А ось те ж саме, але використовуючи нову концепцію асинхронних генераторів:
async def ticker(delay, to):
"""Повертає числа від 0 до `to` кожні `delay` секунд."""
for i in range(to):
yield i
await asyncio.sleep(delay)
Заманливо, чи не так?
Специфікація
В Python генератором є люба функція, що має один або більше yield
:
def func(): # звичайна функція
return
def genfunc(): # генератор
yield
Пропонується використовувати схожу концепцію і для асинхронних генераторів:
async def coro(): # звичайна співпрограма
await smth()
async def asyncgen(): # асинхронний генератор
await smth()
yield 42
Результатом виклику такого генератору буде повернення об'єкту асинхронного генератора, що реалізує інтерфейс асинхронного ітератора, що описаний тут: PEP 492.
Зауважте, що при використанні не пустого return
в такому генераторі буде викликано SyntaxError
.
Зворотня сумісність
Новий синтаксис повністю сумісний з минулими версіями Python. При оголошенні в Python 3.5 async def
функції, що містить в своєму тілі yield
буде викликано SyntaxError
. В Python 3.6 такий синтаксис є повністю допустимим.
Ще декілька слів про продуктивність
Відносно звичайних генераторів
При використанні CPython (еталонна реалізація інтепретатора Python) асинхронні генератори працюють так само швидко як і звичайні (був використаний таки міні бенчмарк, як нижче).
def gen():
i = 0
while i < 100000000:
yield i
i += 1
list(gen())
Відносно асинхронних ітераторів
Наступний міні-бенчмарк, показує, що асинхронні генератори приблизно в 2.3 рази швидше, ніж та сама реалізація з асинхронними ітераторами. А ще вони виглядають лаконічніше.
N = 10 ** 7
async def agen():
for i in range(N):
yield i
class AIter:
def __init__(self):
self.i = 0
def __aiter__(self):
return self
async def __anext__(self):
i = self.i
if i >= N:
raise StopAsyncIteration
self.i += 1
return i
Інші аспекти
Асинхронні генератори списків, кортежів та словників
Не дивлячись на схожу назву, це зовсім різні концепції і вона повинна бути описана в окремому PEP.
Асинхронний yield from
Реалізувати yield from
в теорії можливо, але це потребує дуже сильної переробки всієї концепції генераторів. Да і немає потреби в написанні механізму для реалізації ще одного протоколу для співпрограм, що працює поверх звичайних співпрограм.
Трошки прикладів
Ось приклад, що ілюструє роботу асинхронних генераторів. Він друкує числа від 0 до 9 з інтервалом в одну секунду.
async def ticker(delay, to):
for i in range(to):
yield i
await asyncio.sleep(delay)
async def run():
async for i in ticker(1, 10):
print(i)
import asyncio
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(run())
finally:
loop.close()
Ще немає коментарів