[Python] Шаблонные функции в Python, которые могут выполнятся синхронно и асинхронно
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Сейчас практически каждый разработчик знаком с понятием «асинхронность» в программировании. В эру, когда информационные продукты настолько востребованы, что вынуждены обрабатывать одновременно огромное количество запросов и также параллельно взаимодействовать с большим набором других сервисов — без асинхронного программирования — никуда. Потребность оказалась такой большой, что был даже создан отдельный язык, главной фишкой которого (помимо минималистичности) является очень оптимизированная и удобная работа с параллельным/конкурентным кодом, а именно Golang. Несмотря на то, что статья совершенно не про него, я буду часто делать сравнения и ссылаться. Но вот в Python, про который и пойдёт речь в этой статье — есть некоторые проблемы, которые я опишу и предложу решение одной из них. Если заинтересовала эта тема — прошу под кат.
Так получилось, что мой любимый язык, с помощью которого я работаю, реализую пет-проекты и даже отдыхаю и расслабляюсь — это Python. Я бесконечно покорён его красотой и простотой, его очевидностью, за которой с помощью различного рода синтаксического сахара скрываются огромные возможности для лаконичного описания практически любой логики, на которую способно человеческое воображение. Где-то даже читал, что Python называют сверхвысокоуровневым языком, так как с его помощью можно описывать такие абстракции, которые на других языках описать будет крайне проблематично.
Но есть один серьёзный нюанс — Python очень тяжело вписывается в современные представления о языке с возможностью реализации параллельной/конкурентной логики. Язык, идея которого зародилась ещё в 80-ых годах и являющийся ровесником Java до определённого времени не предполагал выполнение какого-либо кода конкурентно. Если JavaScript изначально требовал конкурентности для неблокирующей работы в браузере, а Golang — совсем свежий язык с реальным пониманием современных потребностей, то перед Python таких задач ранее не стояло.
Это, конечно же, моё личное мнение, но мне кажется, что Python очень сильно опоздал с реализацией асинхронности, так как появление встроенной библиотеки asyncio было, скорее, реакцией на появление других реализаций конкуретного выполнения кода для Python. По сути, asyncio создан для поддержки уже существующих реализаций и содержит не только собственную реализацию событийного цикла, но также и обёртку для других асинхронных библиотек, тем самым предлагая общий интерфейс для написания асинхронного кода. И Python, который изначально создавался как максимально лаконичный и читабельный язык из-за всех перечисленных выше факторов при написании асинхронного кода становится нагромождением декораторов, генераторов и функций. Ситуацию немного исправило добавление специальных директив async и await (как в JavaScript, что важно), но общие проблемы остались.
Я не буду перечислять их все и остановлюсь на одной, которую я и попытался решить: это описание общей логики для асинхронного и синхронного выполнения. Например, если я захочу в Golang запустить функцию параллельно, то мне достаточно просто вызвать функцию с директивой go:
Параллельное выполнение function в Golang
SPL
package main
import "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
SPL
package main
import "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
SPL
import aiohttp
import requests
import pysyncasync as psa
# Регистрируем функцию для синхронного запроса в google
# В скобочках указываем имя для дальнейшего указания в объекте Call
@psa.register("google_request")
def sync_google_request(query, start):
response = requests.get(
url="https://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="https://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 += 10
if __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
===========
Похожие новости:
- [Python] aio api crawler
- Выпуск Pyston 2, реализации языка Python с JIT-компилятором
- [Python, Big Data, Машинное обучение] AutoVIML: Автоматизированное машинное обучение (перевод)
- [Python, Алгоритмы, Машинное обучение, Data Engineering] Расширение возможностей алгоритмов Машинного Обучения с помощью библиотеки daal4py
- [Python, JavaScript, Java] Разбор вступительных задач Школы Программистов hh.ru
- [Разработка веб-сайтов, Python, Django] Что происходит, когда вы выполняете manage.py test? (перевод)
- [Python, Программирование, Математика, Научно-популярное, Физика] Изучаем распространение радиосигналов в ионосфере с помощью SDR
- [Python, Информационная безопасность] Сказ о том, как я токен в Линуксе хранил
- [Python] Мелкая питонячая радость #11: реактивное программирование, парсинг страниц и публикация моделей машинного обучения
- [Высокая производительность, Python, Программирование, Машинное обучение] Deep Learning Inference Benchmark — измеряем скорость работы моделей глубокого обучения
Теги для поиска: #_python, #_python, #_asyncio, #_asynchronous, #_generators, #_python
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:26
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Сейчас практически каждый разработчик знаком с понятием «асинхронность» в программировании. В эру, когда информационные продукты настолько востребованы, что вынуждены обрабатывать одновременно огромное количество запросов и также параллельно взаимодействовать с большим набором других сервисов — без асинхронного программирования — никуда. Потребность оказалась такой большой, что был даже создан отдельный язык, главной фишкой которого (помимо минималистичности) является очень оптимизированная и удобная работа с параллельным/конкурентным кодом, а именно Golang. Несмотря на то, что статья совершенно не про него, я буду часто делать сравнения и ссылаться. Но вот в Python, про который и пойдёт речь в этой статье — есть некоторые проблемы, которые я опишу и предложу решение одной из них. Если заинтересовала эта тема — прошу под кат. Так получилось, что мой любимый язык, с помощью которого я работаю, реализую пет-проекты и даже отдыхаю и расслабляюсь — это Python. Я бесконечно покорён его красотой и простотой, его очевидностью, за которой с помощью различного рода синтаксического сахара скрываются огромные возможности для лаконичного описания практически любой логики, на которую способно человеческое воображение. Где-то даже читал, что Python называют сверхвысокоуровневым языком, так как с его помощью можно описывать такие абстракции, которые на других языках описать будет крайне проблематично. Но есть один серьёзный нюанс — Python очень тяжело вписывается в современные представления о языке с возможностью реализации параллельной/конкурентной логики. Язык, идея которого зародилась ещё в 80-ых годах и являющийся ровесником Java до определённого времени не предполагал выполнение какого-либо кода конкурентно. Если JavaScript изначально требовал конкурентности для неблокирующей работы в браузере, а Golang — совсем свежий язык с реальным пониманием современных потребностей, то перед Python таких задач ранее не стояло. Это, конечно же, моё личное мнение, но мне кажется, что Python очень сильно опоздал с реализацией асинхронности, так как появление встроенной библиотеки asyncio было, скорее, реакцией на появление других реализаций конкуретного выполнения кода для Python. По сути, asyncio создан для поддержки уже существующих реализаций и содержит не только собственную реализацию событийного цикла, но также и обёртку для других асинхронных библиотек, тем самым предлагая общий интерфейс для написания асинхронного кода. И Python, который изначально создавался как максимально лаконичный и читабельный язык из-за всех перечисленных выше факторов при написании асинхронного кода становится нагромождением декораторов, генераторов и функций. Ситуацию немного исправило добавление специальных директив async и await (как в JavaScript, что важно), но общие проблемы остались. Я не буду перечислять их все и остановлюсь на одной, которую я и попытался решить: это описание общей логики для асинхронного и синхронного выполнения. Например, если я захочу в Golang запустить функцию параллельно, то мне достаточно просто вызвать функцию с директивой go: Параллельное выполнение function в GolangSPLpackage main
import "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 в GolangSPLpackage main
import "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 с помощью pySyncAsyncSPLimport aiohttp
import requests import pysyncasync as psa # Регистрируем функцию для синхронного запроса в google # В скобочках указываем имя для дальнейшего указания в объекте Call @psa.register("google_request") def sync_google_request(query, start): response = requests.get( url="https://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="https://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 += 10 if __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 =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:26
Часовой пояс: UTC + 5