Русский
Русский
English
Статистика
Реклама

Шаблонные функции в Python, которые могут выполнятся синхронно и асинхронно

image

Сейчас практически каждый разработчик знаком с понятием асинхронность в программировании. В эру, когда информационные продукты настолько востребованы, что вынуждены обрабатывать одновременно огромное количество запросов и также параллельно взаимодействовать с большим набором других сервисов без асинхронного программирования никуда. Потребность оказалась такой большой, что был даже создан отдельный язык, главной фишкой которого (помимо минималистичности) является очень оптимизированная и удобная работа с параллельным/конкурентным кодом, а именно Golang. Несмотря на то, что статья совершенно не про него, я буду часто делать сравнения и ссылаться. Но вот в Python, про который и пойдёт речь в этой статье есть некоторые проблемы, которые я опишу и предложу решение одной из них. Если заинтересовала эта тема прошу под кат.



Так получилось, что мой любимый язык, с помощью которого я работаю, реализую пет-проекты и даже отдыхаю и расслабляюсь это Python. Я бесконечно покорён его красотой и простотой, его очевидностью, за которой с помощью различного рода синтаксического сахара скрываются огромные возможности для лаконичного описания практически любой логики, на которую способно человеческое воображение. Где-то даже читал, что Python называют сверхвысокоуровневым языком, так как с его помощью можно описывать такие абстракции, которые на других языках описать будет крайне проблематично.

Но есть один серьёзный нюанс Python очень тяжело вписывается в современные представления о языке с возможностью реализации параллельной/конкурентной логики. Язык, идея которого зародилась ещё в 80-ых годах и являющийся ровесником Java до определённого времени не предполагал выполнение какого-либо кода конкурентно. Если JavaScript изначально требовал конкурентности для неблокирующей работы в браузере, а Golang совсем свежий язык с реальным пониманием современных потребностей, то перед Python таких задач ранее не стояло.

Это, конечно же, моё личное мнение, но мне кажется, что Python очень сильно опоздал с реализацией асинхронности, так как появление встроенной библиотеки asyncio было, скорее, реакцией на появление других реализаций конкуретного выполнения кода для Python. По сути, asyncio создан для поддержки уже существующих реализаций и содержит не только собственную реализацию событийного цикла, но также и обёртку для других асинхронных библиотек, тем самым предлагая общий интерфейс для написания асинхронного кода. И Python, который изначально создавался как максимально лаконичный и читабельный язык из-за всех перечисленных выше факторов при написании асинхронного кода становится нагромождением декораторов, генераторов и функций. Ситуацию немного исправило добавление специальных директив async и await (как в JavaScript, что важно) (исправил, спасибо пользователю tmnhy), но общие проблемы остались.

Я не буду перечислять их все и остановлюсь на одной, которую я и попытался решить: это описание общей логики для асинхронного и синхронного выполнения. Например, если я захочу в Golang запустить функцию параллельно, то мне достаточно просто вызвать функцию с директивой go:

Параллельное выполнение function в Golang
package mainimport "fmt"func function(index int) {    fmt.Println("function", index)}func main() {    for i := 0; i < 10; i++ {         go function(i)    }    fmt.Println("end")}


При этом в Golang я могу запустить эту же функцию синхронно:

Последовательное выполнение function в Golang
package mainimport "fmt"func function(index int) {    fmt.Println("function", index)}func main() {    for i := 0; i < 10; i++ {         function(i)    }    fmt.Println("end")}


В Python все корутины (асинхронные функции) основаны на генераторах и переключение между ними происходит во время вызова блокирующих функций, возвращая управление событийному циклу с помощью директивы yield. Честно признаюсь, я не знаю, как работает параллельность/конкурентность в Golang, но не ошибусь, если скажу, что работает это совсем не так, как в Python. Несмотря даже на существующие различия во внутренностях реализации компилятора Golang и интерпретатора CPython и на недопустимость сравнения параллельности/конкурентности в них, всё же я это сделаю и обращу внимание не на само выполнение, а именно на синтаксис. В Python я не могу взять функцию и запустить её параллельно/конкурентно одним оператором. Чтобы моя функция смогла работать асинхронно, я должен явно прописать перед её объявлением async и после этого она уже не является просто функцией, она является уже корутиной. И я не могу без дополнительных действий смешивать их вызовы в одном коде, потому что функция и корутина в Python совсем разные вещи, несмотря на схожесть в объявлении.

def func1(a, b):    func2(a + b)    await func3(a - b)  # Ошибка, так как await может выполняться только в корутинах

Моей основной проблемой оказалась необходимость разрабатывать логику, которая может работать и синхронно, и асинхронно. Простым примером является моя библиотека по взаимодействию с Instagram, которую я давно забросил, но сейчас снова за неё взялся (что и сподвигло меня на поиск решения). Я хотел реализовать в ней возможность работать с API не только синхронно, но и асинхронно, и это было не просто желание при сборе данных в Интернет можно отправить большое количество запросов асинхронно и быстрее получить ответ на них всех, но при этом массивный сбор данных не всегда нужен. В данный момент в библиотеке реализовано следующее: для работы с Instagram есть 2 класса, один для синхронной работы, другой для асинхронной. В каждом классе одинаковый набор методов, только в первом методы синхронные, а во втором асинхронные. Каждый метод выполняет одно и то же за исключением того, как отправляются запросы в Интернет. И только из-за различий одного блокирующего действия мне пришлось практически полностью продублировать логику в каждом методе. Выглядит это примерно так:

class WebAgent:    def update(self, obj=None, settings=None):        ...        response = self.get_request(path=path, **settings)        ...class AsyncWebAgent:    async def update(self, obj=None, settings=None):        ...        response = await self.get_request(path=path, **settings)        ...

Всё остальное в методе update и в корутине update абсолютно идентичное. А как многие знают, дублирование кода добавляет очень много проблем, особенно остро это ощущается в исправлении багов и тестировании.

Для решения этой проблемы я написал собственную библиотеку pySyncAsync. Идея такова вместо обычных функций и корутин реализуется генератор, в дальнейшем я буду называть его шаблоном. Для того, чтобы выполнить шаблон его нужно сгенерировать как обычную функцию или как корутину. Шаблон при выполнении в тот момент, когда ему нужно выполнить внутри себя асинхронный или синхронный код возвращает с помощью yield специальный объект Call, который указывает, что именно вызвать и с какими аргументами. В зависимости от того, как будет сгенерирован шаблон как функция или как корутина таким образом и будут выполнятся методы, описанные в объекте Call.

Покажу небольшой пример шаблона, который предполагает возможность делать запросы в google:

Пример запросов в google с помощью pySyncAsync
import aiohttpimport requestsimport pysyncasync as psa# Регистрируем функцию для синхронного запроса в google# В скобочках указываем имя для дальнейшего указания в объекте Call@psa.register("google_request")def sync_google_request(query, start):    response = requests.get(        url="http://personeltest.ru/aways/google.com/search",        params={"q": query, "start": start},    )    return response.status_code, dict(response.headers), response.text# Регистрируем корутину для асинхронного запроса в google# В скобочках указываем имя для дальнейшенго указания в объекте Call@psa.register("google_request")async def async_google_request(query, start):    params = {"q": query, "start": start}    async with aiohttps.ClientSession() as session:        async with session.get(url="http://personeltest.ru/aways/google.com/search", params=params) as response:            return response.status, dict(response.headers), await response.text()# Шаблон для получения первых 100 результатовdef google_search(query):    start = 0    while start < 100:        # В Call аргументы передавать можно как угодно, они так же и будут переданы в google_request        call = Call("google_request", query, start=start)        yield call        status, headers, text = call.result        print(status)        start += 10if __name__ == "__main__":    # Синхронный запуск кода    sync_google_search = psa.generate(google_search, psa.SYNC)    sync_google_search("Python sync")    # Асинхронный запуск кода    async_google_search = psa.generate(google_search, psa.ASYNC)    loop = asyncio.get_event_loop()    loop.run_until_complete(async_google_search("Python async"))


Расскажу немного про внутреннее устройство библиотеки. Есть класс Manager, в котором регистрируются функции и корутины для вызова с помощью Call. Также есть возможность регистрировать шаблоны, но это необязательно. У класса Manager есть методы register, generate и template. Эти же методы в примере выше вызывались напрямую из pysyncasync, только они использовали глобальный экземпляр класса Manager, который уже создан в одном из модулей библиотеки. По факту можно создать свой экземпляр и от него вызывать методы register, generate и template, таким образом изолируя менеджеры друг от друга, если, например, возможен конфликт имён.

Метод register работает как декоратор и позволяет зарегистрировать функцию или корутину для дальнейшего вызова из шаблона. Декоратор register принимает в качестве аргумента имя, под которым в менеджере регистрируется функция или корутина. Если имя не указано, то функция или корутина регистрируется под своим именем.

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

Метод generate позволяет на основе шаблона сгенерировать функцию или корутину. Принимает два аргумента: первый имя шаблона или сам шаблон, второй sync или async во что генерировать шаблон в функцию или в корутину. На выходе метод generate отдаёт готовую функцию или корутину.

Приведу пример генерации шаблона, например, в корутину:

def _async_generate(self, template):    async def wrapper(*args, **kwargs):        ...        for call in template(*args, **kwargs):            callback = self._callbacks.get(f"{call.name}:{ASYNC}")            call.result = await callback(*call.args, **call.kwargs)        ...    return wrapper

Внутри генерируется корутина, которая просто итерируется по генератору и получает объекты класса Call, потом берёт ранее зарегистрированную корутину по имени (имя берёт из call), вызывает её с аргументами (которые тоже берёт из call) и результат выполнения этой корутины также сохраняет в call.

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

Некоторые нюансы я опустил, но суть, надеюсь, в общем донёс.

Если быть честным, эта статья была написана мною скорее для того, чтобы поделится своими мыслями о решении проблем с асинхронным кодом в Python и, самое главное, выслушать мнения хабравчан. Возможно, кого-то я натолкну на другое решение, возможно, кто-то не согласится именно с данной реализацией и подскажет, как можно сделать её лучше, возможно, кто-то расскажет, почему такое решение вообще не нужно и не стоит смешивать синхронный и асинхронный код, мнение каждого из вас для меня очень важно. Также я не претендую на истинность всех моих рассуждений в начале статьи. Я очень обширно размышлял на тему других ЯП и мог ошибиться, плюс есть возможность, что я могу путать понятия, прошу, если вдруг будут замечены какие-то несоответствия опишите в комментарии. Также буду рад, если будут поправки по синтаксису и пунктуации.

И спасибо за внимание к данному вопросу и к этой статье в частности!
Источник: habr.com
К списку статей
Опубликовано: 02.11.2020 00:07:43
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Python

Asyncio

Asynchronous

Generators

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru