Асинхронное API
Введение в асинхронное программирование
Асинхронное программирование стало неотъемлемой частью современной Python-разработки и продолжает набирать популярность среди веб-разработчиков.
Основные темы
Итераторы, генераторы и корутины
Итераторы
Итераторы - фундаментальная концепция Python, которую разработчики используют ежедневно, часто не задумываясь об их работе. Любая коллекция в Python (списки, словари, множества, строки, файлы) является итерабельной.
Реализация аналога функции range():
class Range:
def __init__(self, stop_value: int):
self.current = -1
self.stop_value = stop_value - 1
def __iter__(self):
return RangeIterator(self)
class RangeIterator:
def __init__(self, container):
self.container = container
def __next__(self):
if self.container.current < self.container.stop_value:
self.container.current += 1
return self.container.current
raise StopIteration
Упрощенная версия:
class Range2:
def __init__(self, stop_value: int):
self.current = -1
self.stop_value = stop_value - 1
def __iter__(self):
return self
def __next__(self):
if self.current < self.stop_value:
self.current += 1
return self.current
raise StopIteration
Как работает цикл for под капотом:
iterable = Range2(5)
iterator = iter(iterable)
while True:
try:
value = next(iterator)
print(value)
except StopIteration:
break
Генераторы
Генераторы работают на принципе запоминания контекста выполнения функции с помощью ключевого слова yield.
Простой пример генератора:
def simple_generator():
yield 1
yield 2
return 3
gen = simple_generator()
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # StopIteration: 3
Генераторные выражения:
gen_exp = (x for x in range(100000))
print(gen_exp) # <generator object <genexpr> at 0x...>
Синтаксический сахар yield from:
numbers = [1, 2, 3]
# Стандартный подход
def func():
for item in numbers:
yield item
# Упрощенный подход
def func():
yield from numbers
Корутины
Корутины - основные строительные блоки асинхронного программирования, появившиеся как решение проблемы GIL (Global Interpreter Lock).
Пример корутины для финансовых расчетов:
import math
def cash_return_coro(percent: float, years: int) -> float:
value = math.pow(1 + percent / 100, years)
while True:
try:
deposit = (yield)
yield round(deposit * value, 2)
except GeneratorExit:
print('Выход из корутины')
raise
# Использование
coro = cash_return_coro(5, 5)
next(coro)
values = [1000, 2000, 5000, 10000, 100000]
for item in values:
print(coro.send(item))
next(coro)
coro.close()
Асинхронность в Python и asyncio
Типы задач
- CPU bound-задачи - интенсивное использование процессора (математические модели, нейросети, рендеринг)
- I/O bound-задачи - основная работа с вводом/выводом (файловая система, сеть)
- Memory bound-задачи - интенсивная работа с оперативной памятью
Проблема блокирующих операций
import requests
def do_some_logic(data):
pass
def save_to_database(data):
pass
# Блокирующий код
data = requests.get('https://data.aggregator.com/films')
processed_data = do_some_logic(data)
save_to_database(data)
Event Loop - сердце асинхронных программ
Базовая реализация планировщика:
import logging
from typing import Generator
from queue import Queue
class Scheduler:
def __init__(self):
self.ready = Queue()
self.task_map = {}
def add_task(self, coroutine: Generator) -> int:
new_task = Task(coroutine)
self.task_map[new_task.tid] = new_task
self.schedule(new_task)
return new_task.tid
def exit(self, task: Task):
del self.task_map[task.tid]
def schedule(self, task: Task):
self.ready.put(task)
def _run_once(self):
task = self.ready.get()
try:
result = task.run()
except StopIteration:
self.exit(task)
return
self.schedule(task)
def event_loop(self):
while self.task_map:
self._run_once()
Реализация задачи (Task):
import types
from typing import Generator, Union
class Task:
task_id = 0
def __init__(self, target: Generator):
Task.task_id += 1
self.tid = Task.task_id
self.target = target
self.sendval = None
self.stack = []
def run(self):
while True:
try:
result = self.target.send(self.sendval)
if isinstance(result, types.GeneratorType):
self.stack.append(self.target)
self.sendval = None
self.target = result
else:
if not self.stack:
return
self.sendval = result
self.target = self.stack.pop()
except StopIteration:
if not self.stack:
raise
self.sendval = None
self.target = self.stack.pop()
Asyncio
С версии Python 3.5 появился синтаксис async/await для нативных корутин.
Простая программа с asyncio:
import random
import asyncio
async def func():
r = random.random()
await asyncio.sleep(r)
return r
async def value():
result = await func()
print(result)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(value())
loop.close()
Основные функции asyncio:
gather- одновременное выполнение корутинsleep- приостановка выполненияwait/wait_for- ожидание выполнения корутин
Основные функции event_loop:
get_event_loop- получение объекта цикла событийrun_until_complete/run- запуск асинхронных функцийshutdown_asyncgens- корректное завершениеcall_soon- планирование выполнения
Асинхронные фреймворки
Twisted
Один из старейших асинхронных фреймворков с собственной реализацией event-loop.
Основные концепции:
- Protocol - описание получения и отправки данных
- Factory - управление созданием объектов протокола
- Reactor - собственная реализация event-loop
- Deferred-объекты - цепочки обратных вызовов
Пример Deferred-объекта:
from twisted.internet import defer
def toint(data):
return int(data)
def increment_number(data):
return data + 1
def print_result(data):
print(data)
def handleFailure(f):
print("OOPS!")
def get_deferred():
d = defer.Deferred()
return d.addCallbacks(toint, handleFailure)\
.addCallbacks(increment_number, handleFailure)\
.addCallback(print_result)
Aiohttp
Асинхронные HTTP-клиент и сервер, построенные поверх asyncio.
Пример приложения:
import aiohttp
from aiohttp import web
async def get_phrase():
async with aiohttp.ClientSession() as session:
async with session.get('https://fish-text.ru/get',
params={'type': 'title'}) as response:
result = await response.json(content_type='text/html; charset=utf-8')
return result.get('text')
async def index_handler(request):
return web.Response(text=await get_phrase())
async def response_signal(request, response):
response.text = response.text.upper()
return response
async def make_app():
app = web.Application()
app.on_response_prepare.append(response_signal)
app.add_routes([web.get('/', index_handler)])
return app
web.run_app(make_app())
FastAPI
Современный фреймворк для быстрой разработки API, построенный на Starlette и Pydantic.
Простой пример API:
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional
app = FastAPI(title="Простые математические операции")
class Add(BaseModel):
first_number: int = Field(title='Первое слагаемое')
second_number: Optional[int] = Field(title='Второе слагаемое')
class Result(BaseModel):
result: int = Field(title='Результат')
@app.post("/add", response_model=Result)
async def create_item(item: Add):
return {
'result': item.first_number + (item.second_number or 1)
}
Заключение
Вы познакомились с основами асинхронного программирования в Python, изучили ключевые концепции итераторов, генераторов и корутин, освоили работу с asyncio и популярными асинхронными фреймворками.
Полезные ссылки
- https://dbader.org/blog/python-iterators
- https://www.techbeamers.com/python-iterator/
- https://realpython.com/introduction-to-python-generators/
- https://www.python.org/dev/peps/pep-0255
- https://ru.wikipedia.org/wiki/Ленивые_вычисления
- https://medium.com/@chandansingh_99754/python-generators-and-coroutines-d54ed9c343ae
- https://www.youtube.com/watch?v=AXkOli6BsBY
- https://realpython.com/python-sockets/#reference
- https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/