Я хочу рассказать, как я пытался изобрести идеальный апи для своей библиотеки.
ДИСКЛЕЙМЕР
- я понимаю что далеко не всем это интересно, и у нас в клубе не так много чисто технических постов, но вроде как это не возбраняется
- я не хочу идти с этим на хабр (по крайней мере, пока): у меня нет опыта написания технических статей, и, откровенно говоря, хочется для начала попробовать в уютном клубе
- я прекрасно понимаю, что все что я сейчас буду описывать, либо уже придумано до меня, либо никому не нужно, но это не важно: я просто делаю удобный мне инструмент, попутно осваивая технологии
Исходные данные
- у меня есть несколько пет-проектов в виде ботов для телеги
- все они написаны с использованием python + aiogram
- я люблю упарываться по статистике (см. предыдущий пост) использования своих ботов
Поскольку я пришел к выводу, что удобнее всего писать ивенты в базу, а потом их анализировать, я решил написать простенькую библиотеку.
Что бы мне хотелось: один раз инициализировать библиотеку на старте приложения, и дальше кидать в нее ивенты.
Что у меня получилось? Да примерно то, что я и хотел:
- на старте приложения библиотека инициализируется (надо указать url для базы и название таблицы для хранения ивентов)
- любую функцию (и хендлер сообщений) я помечаю декоратором
@track('/start command')
. Можно также использовать отдельную функциюmanual_track
чтобы передать ивент напрямую - благодяря
asyncio.create_task()
сохранение в базу будет происходить в фоне, и основной процесс не будет задерживаться для того чтобы сходить в базу - благодаря тому, что aiogram позволяет достать из контекста потока практически все, ивент обогащается всякими полезными данными без необходимости пробрасывать их все как аргументы
- ???
- PROFIT
Все, первая версия библиотеки уже работает и все замечательно... почти
Идеальный API для библиотеки
Это уже перфекционизм, возможно даже нездоровый, но мне очень хочется, чтобы для того, чтобы сохранить ивент, у меня была одна универсальная функция.
То, что есть в первой версии, уже довольно универсально: этот декоратор можно вешать на любую функцию, в независимости от того, асинхронная она или нет. Даже под dependency injection aiogram'a она подстраивается.
Но очень хочется, чтобы можно было этот же декоратор использовать как обычную функцию. Поэтому дальше я расскажу про то, как я попытался этого добиться.
Кратко про декораторы
Полагаю, что если вы дочитали досюда, вы и так знаете, что такое декораторы в питоне. Я только напомню, как выглядит декоратор, который может работать с параметрами и без:
...и его использование:
Проблема
заключается в том, что после того как в декораторе появились аргументы, уже не получится простыми методами однозначно понять, кто вызвал функцию — декоратор, или обычный вызов.
За пару вечеров попыток, я пришел к двум решениям:
Решение 1: красиво снаружи, мерзко внутри, медленно
С помощью inspect можно понять, как выглядит вызов функции, и тупо посмотреть, стоит ли там символ декоратора:
Это УСЛОВНО медленно:
На 10к тестовых запусках, декорированная функция отрабатывает в среднем за 0.000006с, а вызванная напрямую и проинспкетированная - за 0.002с. Разница в 300-400 раз.
С другой стороны — это не высоконагруженное приложение, а бот для телеги, и микро- и наносекунды здесь никто считать не будет. Зато какой красивый api получается!
Решение 2: некрасиво снаружи, красиво внутри, быстро
В принципе, это даже не очень спортивно:
Вот где-то здесь я и остановился.
Мне бы очень хотелось послушать совета более опытных программистов, потому что я не могу определить, какое решение мне нравится - красивое внешне или простое внутри.
Спасибо что дочитали!
Есть клёвая лекция про декораторы. Добавьте себе
@functools.wraps
в декоратор, чтобы всякие штуки для метапрограммирования с декорированной функцией продолжали работать.А так мне кажется, что использовать одну и ту же штуку и для декораторов с логикой "оберни функцию в такую вот штуку" и для "запусти штуку прямо сейчас" может быть слишком магическим. Возможно, я бы скорее сделал отдельную функцию
track
и отдельный декоратор "запусти вот это в начале функции" и получил что-то вроде@at_start_async(track)
.Challenge accepted.
Можно попытаться сделать что-то более красивое через
weakref.finalize
, но explicit is better than implicit, поэтому решение 2 или вариант с отдельными декоратором и функцией для сохранения события мне кажется более правильным.Коммент не про имплементацию, а про изначальную задачу. Немного напомнило вот это: https://www.serverless.com/framework/docs/guides/sdk/python#tag_event - да, там не про декораторы, но возможно приглянется что-то из этих утилиток. Если понатыкать по коду достаточно этих трейсеров, то в дашборде потом вот такие картинки вылазят:
😱 Комментарий удален его автором...