Рубрики
Без рубрики

Python async/await Tutorial

Автор оригинала: Scott Robinson.

Python async/await Tutorial

В последние несколько лет асинхронное программирование набирает все большую популярность, и на то есть веские причины. Хотя это может быть сложнее, чем традиционный линейный стиль, он также гораздо более эффективен.

Например, вместо того чтобы ждать завершения HTTP-запроса перед продолжением выполнения, с помощью асинхронных сопрограмм Python вы можете отправить запрос и выполнить другую работу, ожидающую в очереди завершения HTTP-запроса. Возможно, потребуется немного больше размышлений, чтобы правильно понять логику, но вы сможете справиться с гораздо большей работой с меньшими ресурсами.

Даже тогда синтаксис и выполнение асинхронных функций в таких языках, как Python, на самом деле не так уж сложны. Теперь JavaScript-это совсем другая история, но Python, похоже, выполняет ее довольно хорошо.

Асинхронность, по-видимому, является большой причиной, почему Node.js так популярно для серверного программирования. Большая часть кода, который мы пишем, особенно в тяжелых приложениях ввода-вывода, таких как веб-сайты, зависит от внешних ресурсов. Это может быть что угодно-от удаленного вызова базы данных до отправки в службу REST. Как только вы запросите любой из этих ресурсов, ваш код будет ждать без дела.

При асинхронном программировании вы позволяете вашему коду обрабатывать другие задачи, ожидая ответа этих других ресурсов.

Сопрограммы

Асинхронная функция в Python обычно называется “сопрограммой”, которая представляет собой просто функцию, использующую ключевое слово async , или функцию, украшенную @asyncio.coroutine . Любая из приведенных ниже функций будет работать как сопрограмма и фактически эквивалентна по типу:

import asyncio

async def ping_server(ip):
    pass

@asyncio.coroutine
def load_file(path):
    pass

Это специальные функции, которые возвращают объекты сопрограммы при вызове. Если вы знакомы с обещаниями JavaScript, то можете думать об этом возвращаемом объекте почти как о обещании. Вызов любого из них на самом деле не запускает их, но вместо этого возвращается объект coroutine , который затем может быть передан в цикл событий для последующего выполнения.

В случае, если вам когда-нибудь понадобится определить, является ли функция сопрограммой или нет, asyncio предоставляет метод asyncio.iscoroutinefunction(func) , который делает именно это для вас. Или, если вам нужно определить, является ли объект , возвращаемый из функции , объектом сопрограммы, вы можете использовать asyncio.is вместо этого сопрограмма(obj) .

Выход из

На самом деле существует несколько способов вызова сопрограммы, одним из которых является метод yield from . Это было введено в Python 3.3 и было дополнительно улучшено в Python 3.5 в виде async/await (к которому мы вернемся позже).

Выражение yield from можно использовать следующим образом:

import asyncio

@asyncio.coroutine
def get_json(client, url):
    file_content = yield from load_file('/Users/scott/data.txt')

Как вы можете видеть, yield from используется в функции, украшенной @asyncio.coroutine . Если бы вы попытались использовать yield из вне этой функции, то получили бы такую ошибку от Python:

  File "main.py", line 1
    file_content = yield from load_file('/Users/scott/data.txt')
                  ^
SyntaxError: 'yield' outside function

Чтобы использовать этот синтаксис, он должен быть внутри другой функции (обычно с декоратором сопрограммы).

Асинхронность/ожидание

Более новый и чистый синтаксис заключается в использовании ключевых слов async/await . Введенный в Python 3.5, async используется для объявления функции как сопрограммы, очень похожей на то, что делает декоратор @asyncio.coroutine . Его можно применить к функции, поместив ее в начало определения:

async def ping_server(ip):
    # ping code here...

Чтобы на самом деле вызвать эту функцию, мы используем await вместо yield from , но во многом таким же образом:

async def ping_local():
    return await ping_server('192.168.1.1')

Опять же, как и yield from , вы не можете использовать это вне другой сопрограммы, иначе вы получите синтаксическую ошибку.

В Python 3.5 поддерживаются оба способа вызова сопрограмм, но способ async/await должен быть основным синтаксисом.

Запуск цикла событий

Ни одна из описанных выше сопрограмм не будет иметь значения (или работать), если вы не знаете, как запустить и запустить цикл событий . Цикл событий является центральной точкой выполнения асинхронных функций, поэтому, когда вы хотите фактически выполнить сопрограмму, вы будете использовать именно ее.

Цикл событий предоставляет вам довольно много возможностей:

  • Регистрация, выполнение и отмена отложенных вызовов (асинхронные функции)
  • Создание клиентских и серверных транспортов для связи
  • Создание подпроцессов и транспортов для связи с другой программой
  • Делегирование вызовов функций пулу потоков

Хотя на самом деле существует довольно много конфигураций и типов циклов событий, которые вы можете использовать, большинству программ, которые вы пишете, просто нужно будет использовать что-то подобное для планирования функции:

import asyncio

async def speak_async():
    print('OMG asynchronicity!')

loop = asyncio.get_event_loop()
loop.run_until_complete(speak_async())
loop.close()

Последние три строки-это то, что нас здесь интересует. Он начинается с получения цикла событий по умолчанию ( asyncio.get_event_loop() ), планирования и запуска asynctask, а затем закрытия цикла, когда цикл будет завершен.

Функция loop.run_until_complete() фактически блокируется, поэтому она не вернется до тех пор, пока не будут выполнены все асинхронные методы. Поскольку мы запускаем его только в одном потоке, он никак не может двигаться вперед, пока цикл выполняется.

Теперь вы можете подумать, что это не очень полезно, так как мы все равно блокируем цикл событий (а не только вызовы ввода-вывода), но представьте себе, что вся ваша программа обернута в асинхронную функцию, которая затем позволит вам запускать много асинхронных запросов одновременно, как на веб-сервере.

Вы даже можете разорвать цикл событий на его собственный поток, позволив ему обрабатывать все большие запросы ввода-вывода, в то время как основной поток обрабатывает логику программы или пользовательский интерфейс.

Пример

Итак, давайте рассмотрим немного больший пример, который мы действительно можем запустить. Следующий код представляет собой довольно простую асинхронную программу, которая извлекает JSON из Reddit, анализирует JSON и распечатывает лучшие сообщения дня из/r/python,/r/programming и/r/compsci.

Первый показанный метод, get_json() , вызывается get_reddit_top() и просто создает HTTP GET-запрос на соответствующий URL Reddit. Когда это вызывается с помощью await , цикл событий может затем продолжить работу и обслуживать другие сопрограммы, ожидая возвращения HTTP-ответа. Как только это произойдет, JSON возвращается в get_reddit_top() , анализируется и распечатывается.

import signal
import sys
import asyncio
import aiohttp
import json

loop = asyncio.get_event_loop()
client = aiohttp.ClientSession(loop=loop)

async def get_json(client, url):
    async with client.get(url) as response:
        assert response.status == 200
        return await response.read()

async def get_reddit_top(subreddit, client):
    data1 = await get_json(client, 'https://www.reddit.com/r/' + subreddit + '/top.json?sort=top&t=day&limit=5')

    j = json.loads(data1.decode('utf-8'))
    for i in j['data']['children']:
        score = i['data']['score']
        title = i['data']['title']
        link = i['data']['url']
        print(str(score) + ': ' + title + ' (' + link + ')')

    print('DONE:', subreddit + '\n')

def signal_handler(signal, frame):
    loop.stop()
    client.close()
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)

asyncio.ensure_future(get_reddit_top('python', client))
asyncio.ensure_future(get_reddit_top('programming', client))
asyncio.ensure_future(get_reddit_top('compsci', client))
loop.run_forever()

Это немного отличается от примера кода, который мы показывали ранее. Чтобы получить несколько сопрограмм, работающих в цикле событий, мы используем asyncio.ensure_future () , а затем запускаем цикл навсегда, чтобы обработать все.

Чтобы запустить это, вам нужно сначала установить aiohttp , что вы можете сделать с помощью PIP:

$ pip install aiohttp

Теперь просто убедитесь, что вы запускаете его с Python 3.5 или выше, и вы должны получить такой вывод:

$ python main.py
46: Python async/await Tutorial (http://stackabuse.com/python-async-await-tutorial/)
16: Using game theory (and Python) to explain the dilemma of exchanging gifts. Turns out: giving a gift probably feels better than receiving one... (http://vknight.org/unpeudemath/code/2015/12/15/The-Prisoners-Dilemma-of-Christmas-Gifts/)
56: Which version of Python do you use? (This is a poll to compare the popularity of Python 2 vs. Python 3) (http://strawpoll.me/6299023)
DONE: python

71: The Semantics of Version Control - Wouter Swierstra (http://www.staff.science.uu.nl/~swier004/Talks/vc-semantics-15.pdf)
25: Favorite non-textbook CS books (https://www.reddit.com/r/compsci/comments/3xag9e/favorite_nontextbook_cs_books/)
13: CompSci Weekend SuperThread (December 18, 2015) (https://www.reddit.com/r/compsci/comments/3xacch/compsci_weekend_superthread_december_18_2015/)
DONE: compsci

1752: 684.8 TB of data is up for grabs due to publicly exposed MongoDB databases (https://blog.shodan.io/its-still-the-data-stupid/)
773: Instagram's Million Dollar Bug? (http://exfiltrated.com/research-Instagram-RCE.php)
387: Amazingly simple explanation of Diffie-Hellman. His channel has tons of amazing videos and only a few views :( thought I would share! (https://www.youtube.com/watch?v=Afyqwc96M1Y)
DONE: programming

Обратите внимание, что если вы выполните это несколько раз, порядок, в котором распечатываются данные субреддита, изменится. Это происходит потому, что каждый из вызовов, которые мы делаем, освобождает (дает) контроль над потоком, позволяя обрабатывать другой HTTP – вызов. Тот, кто вернется первым, будет распечатан первым.

Вывод

Хотя встроенная асинхронная функциональность Python не так гладка, как Javascript, это не означает, что вы не можете использовать ее для интересных и эффективных приложений. Просто потратьте 30 минут, чтобы изучить все его тонкости, и у вас будет гораздо лучшее представление о том, как вы можете интегрировать это в свои собственные приложения.

Что вы думаете о async/await Python? Как вы использовали его в прошлом? Дайте нам знать в комментариях!