python

Асинхронный Python 3.5 и Mongodb

  • вторник, 17 ноября 2015 г. в 02:10:42
http://habrahabr.ru/post/270709/


Это достаточно вольный перевод статьи об основных новшествах асинхронного драйвера для mongodb используемого в tornado. Основной мотив, который послужил для написания этого перевода — новшества, появившиеся в этой версии, такие как поддержка asyncio, async, await и Python 3.5. Сама статья не сколько перечисление новшеств, сколько лаконичные примеры асинхронной работы с MongoDB.


Введение
asyncio
aggregate
Python 3.5
async and await


Введение


Недавно была опубликована новая Beta версия Python драйвера для Mongodb, Motor. В этой версии содержится одно из самых больших обновлений. Для установки можно использовать:
python -m pip install --pre motor==0.5b0
Motor 0.5 по прежнему зависит от PyMongo 2.8.0. Это устаревшая версия PyMongo, но сейчас не было достаточно времени чтоб полностью перейти на третью версию, что простительно, так как этот релиз достаточно большой.

asyncio


Motor теперь может интегрироваться с asyncio, как альтернатива Tornado. Большая благодарность Реми Джолину, Андрею Светлову
svetlov и Николаю Новику за их огромный вклад в интеграцию Motor для работы с asyncio.

API-Интерфейсы Tornado и asyncio являются родственными. Пример Motor с Tornado:
# Tornado API
from tornado import gen, ioloop
from motor.motor_tornado import MotorClient

@gen.coroutine
def f():
    result = yield client.db.collection.insert({'_id': 1})
    print(result)

client = MotorClient()
ioloop.IOLoop.current().run_sync(f)

И здесь пример для asyncio:
import asyncio
from motor.motor_asyncio import AsyncIOMotorClient

@asyncio.coroutine
def f():
    result = yield from client.db.collection.insert({'_id': 1})
    print(result)

client = AsyncIOMotorClient()
asyncio.get_event_loop().run_until_complete(f())

В отличие от Tornado, asyncio не включает реализацию http, а тем более не является фреймворком. Для этого используйте библиотеку aiohttp Андрея Светлова. Небольшой пример для работы Motor с aiohttp.

aggregate


MotorCollection.aggregate теперь по умолчанию возвращает курсор, и курсор возвращается непосредственно без yield. Старый синтаксис больше не поддерживается:
# Motor 0.4 and older, no longer supported.
cursor = yield collection.aggregate(pipeline, cursor={})
while (yield cursor.fetch_next):
    doc = cursor.next_object()
    print(doc)

В Motor 0.5 просто сделайте:
# Motor 0.5: no "cursor={}", no "yield".
cursor = collection.aggregate(pipeline)
while (yield cursor.fetch_next):
    doc = cursor.next_object()
    print(doc)

В asyncio для этого используется yield from:
# Motor 0.5 with asyncio.
cursor = collection.aggregate(pipeline)
while (yield from cursor.fetch_next):
    doc = cursor.next_object()
    print(doc)

Python 3.5


Сейчас Motor совместим с Python 3.5, что потребовало определённых усилий. Это было трудно, потому что Motor не просто работает с сопрограммами (coroutines), он использует сопрограммы внутри себя для реализации некоторых из своих функций, таких как
MotorClient.open и MotorGridFS.put.
Был метод для написания сопрограмм, которые работают в Python 2.6 c Python 3.4, но в Python 3.5 это было окончательно поломано. Нет единого пути для возвращения значений из Python 3.5 нативной сопрограмме или Python 2 генератора базирующегося на сопрограмме, так что все внутренние сопрограммы motor, которые возвращают значения, были переписаны с помощью обратных вызовов.

async and await


Награда за усилия потраченные на интеграцию с Python 3.5, состоит в том что теперь motor работает с родной сопрограммой, написанной с учетом ключевых слов async и await синтаксис:
async def f():
    await collection.insert({'_id': 1})

Курсор из
MotorCollection.find, MotorCollection.aggregate, или MotorGridFS.find может быть красиво и очень эффективно итегрирован в нативных сопрограммах (coroutines) с async for:
async def f():
    async for doc in collection.find():
        print(doc)

Насколько эффективно? Для коллекции из 10 000 документов этот пример кода выполнялся за 0.14 секунды.
# Motor 0.5 with Tornado.
@gen.coroutine
def f():
    cursor = collection.find()
    while (yield cursor.fetch_next):
        doc = cursor.next_object()
        print(doc)


Следующий код, в котором просто заменены gen.coroutine и yield на async и await и выполняет примерно тоже.
# Motor 0.5 with Tornado, using async and await.
async def f():
    cursor = collection.find()
    while (await cursor.fetch_next):
        doc = cursor.next_object()
        print(doc)

Но с async for время работы занимает 0.04 секунды, то есть в три раза быстрее.
# Motor 0.5 with Tornado, using async for.
async def f():
    cursor = collection.find()
    async for doc in cursor:
        print(doc)

Однако, MotorCursor в to_list прежнему играет основную роль:
# Motor 0.5 with Tornado, using to_list.
async def f():
    cursor = collection.find()
    docs = await cursor.to_list(length=100)
    while docs:
        for doc in docs:
            print(doc)
        docs = await cursor.to_list(length=100)

Функция с to_list в два раза быстрее чем асинхронные, но это выглядит не так красиво и требует указания размера чанка. Я думаю, что async for выглядит довольно стильно и работает достаточно быстро для того, чтобы его применять в большинстве случаев.

Бета версии релизов motor публиковались далеко не всегда, но в этот раз по-другому. Интеграция asyncio в motor является совершенно новой. И поскольку это потребовало повсеместного рефакторинга ядра motor, и переписывания существующей интеграции tornado, была выпущена бета-версия для того, чтобы исправить все упущения.

P.S. Просьба о грамматических ошибках и ошибках перевода писать в личку.