Pull to refresh

Comments 36

>> Наверное, первое, что приходит на ум - это Dependency Injector. Но, кажется, создатель отказался от его сопровождения (да и библиотека имеет множество минусов, которые нам тоже не нравятся).

Это не так, DI вполне себе развивается, проверьте гитхаб. На вкус и цвет все фломастеры разные

Ну ничего себе, действительно ожил. Однако, 1 коммит раз в месяц - это не прям "активное развитие". На самом деле, я не против Dependency Injector, просто он очень сильно перегружен, реализация на глобалах и вроде как не thread-safe

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

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

С обоими пунктами у dependency-injector все очень странно. То есть мы можем использовать его декоратор `@inject`, но при этом придется вручную перечислять в функции, что мы финалиизруем, хотя это вообще могут быть транзиитвные завимисоти. Как он переиспользует объекты тоже не оченвидно, так как понятия скоупа фактически нет за пределами этого декоратора.

Не пользуемся инжектом, всё собираем в контейнерах, так оказалось ближе

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

Скажите, а кто-нибудь пробовал завести этот FastApi под msys2? Решил попробовать его под новый пет-проект для построения несложного API вместо привычного в этих случаях php, но сразу же столкнулся с тем, что он не хочет устанавливаться. При попытке установки через pip зачем-то пытается собрать какую-то зависимость из исходников (какой-то maturin, ХЗ, что такое). Потом после гугления обновил пакеты и нашёл, что есть пакет mingw-w64-x86_64-python-fastapi, который вроде бы то, что нужно, но поставляемый с ним инструмент CLI fastapi при запуске сразу же падает, ругаясь на то, что нет модуля fastapi_cli.cli (и его действительно нет, пакет эти каталоги не ставит!) и отправляя опять в pip. Круг замкнулся и не могу это победить уже второй день.

Не очень понятно, что вы делаете с msys2. Ваша целевая платформа для сервера windows?

Ставлю его под msys2. Просто у меня Windows на рабочей машине и издревле я использую msys2 и утилиты из него для всякого рода автоматизации. Скрипты там разные и так далее.

Используйте запуск через uvicorn или любой другой ASGI сервер вместо FastAPI CLI

А поподробнее можно? Что значит запуск через uvicorn? uvicorn и так среди зависимостей ставился, что ещё надо?

Забавно выходит, вы же автор fastdepends, но при этом рекомендуете dishka, а не свой проект.

Ну, то, что он мой - не значит, что он самый-самый). FastDepends - это буквально копия API FastAPI и он грешит теми же проблемами в качестве DI

Но он решает несколько другую проблему, нежели просто DI и нужен он мне именно в таком виде. Все-таки в качестве чистого IoC контейнера dishka значительно лучше.

Позволяет переиспользовать контейнер зависимостей в рамках всего приложения (и других фреймворков/библиотек), а не только handler'ах FastAPI (аккуратнее с этим).

А дайте пример, как мне на слое сервисов достать зависимость, например, сессию к БД. Сейчас приходится её прокидывать от handler'а глубже, хотя в handler'е она точно не нужна.

Так не достать, а передать. =) Прям в инит сервиса/интерактора передать условный DAO. Соответсвтенно в хэндлере никаких сесиий, только инстанс сервиса, а контейнер уже всё связывает друг с другом. Пример есть в документации https://dishka.readthedocs.io/en/stable/quickstart.html

Спасибо за статью

Рекомендую посмотреть https://github.com/nightblure/injection. Более легкая версия dependency-injector, не перегруженная лишним. Еще есть понятный механизм финализации ресурсов и удобное переопределение зависимостей как в dependency-injector

Насколько я помню, в dependency injector финализация как раз сделана очень ограниченно - мы можем финализировать только синглтоны (через Resource), либо при использовании inject забыв про финализацию транзитивных зависимостей.

На мой взгляд, dependency-injector - это очень неудачное решение для контейнера не потому что разработчики перестали его поддерживать, а с точки зрения дизайна API. То есть все клоны будут обраладть теми же пробелмами: слабый контроль жизненного цикла объектов, необходимость ссылаться на конкретный класс контейнере там где мы хотим просто иметь зависимость, неявная передача контейнера через глобальные переменные.

Посмотрите внимательнее на dishka, тут все работает чуточку по другому и я верю, что оно дает больше возможностей и контроля, может и вам зайдет

Финализация с function-скоупом так кажется в принципе не работает) (это про маркер Closing, см. https://python-dependency-injector.ets-labs.org/providers/resource.html)

Пытались в одном из issue с еще одним пользователем это завести - не получилось. но это легко исправить

Не особо понял про слабый контроль жизненного цикла, вроде бы с этим все просто и оно работает. Для меня (как наверное и для большинства) существуют две нужды, то есть два скоупа - request/function (transient скоуп) и синглтоны

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

Здорово, если в дишке нет глобального стейта. Согласен, что это не очень хорошо, но тем не менее с таким контейнером кажется нет никаких проблем, потому в контейнере лежит довольно примитивный и простой код (как, например, в вышеупомянутом injection и https://github.com/modern-python/that-depends)

Ну вот function-scoped зависимости финализировать как раз самое важное на мой взгляд. Приложение может никогда не перезапускаться или умирать только под SIGKILL, а всякие соединения, скрытые за абстракциями, возвращать в луп приходится регулярно.

Про громоздкость API грустно слышать, потому что количество разных сущностей на порядок меньше чем в dependency-injector. Знай дергай себе provide с указанием скоупа, а потом тот же inject. В том время как в dependency-injector десятка два видов фабрик и даже не знаешь какую когда юзать. Возможно дело в сложном квикстарте, если есть советы как улучшить - буду рад услышать

Как видите, приседаний стало меньше

А кажется не стало. Если вам реально нужен DI в вашем приложении, нужно менять реализации в зависимости от среды или фазы луны, наверное всё это нагромождение абстракций имеет какой-то смысл. Но чаще всё это просто не нужно. Зачастую это переусложнение ради "а вдруг когда-нибудь пригодится", потому что умному программисту Васе стало скучно. Например, заморачиваться с "настоящим" DI только ради тестов с моками - это очередное "безумие индустрии". Ведь всё это дополнительный код, дополнительная когнитивная нагрузка, дополнительная поддержка. Надёжнее и правильнее выделить специальное окружение и развернуть в нём всё ваше барахло для интеграционного тестирования, максимально приближенного к жизни.

FastAPI задуман простым, понятным и элегантным, с минимумом бойлерплейта и без нагромождения абстракций и паттернов из мира Java. И этим он снискал такую славу. Люди в конце концов любят простоту. Даже тот, кто когда-то писал сложный и запутанный код, с опытом возвращается к очень простым концепциям.

В одном продукте у нас было примерно 100 KLOC в сервисах на FastAPI и ни разу не понадобилось городить там полноценный DI. Может быть мы что-то делали неправильно, но всё работало, и всё было удобно.

В общем, я бы десять раз подумал прежде чем обмазываться сложными абстракциями, чтобы было "правильно" и по-взрослому. Возможно, вам всё это на самом деле не нужно.

DI это не только про смену реализаций, это ещё и про управление жизненным циклом и умение пробросить параметризованные объекты без протаскивания этих параметров по всем слоям.

Что же касается 2 реализаций, они часто есть - для теста и для прода. Вопрос не про сложные абстракции, вопрос про отделение контрактов от деталей, когда приложение растет, это становится полезным.

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

Но дело ведь не только в "менять реализации в зависимости от среды или фазы луны". Лично я вижу две главные цели таких фреймворков:

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

2. Облегчить тестирование. Тесты становится писать в удовольствие, когда вместо манкипатчинга в каждом тесте можно просто вызвать метод типа override, который подменит объект везде, где использовался связанный провайдер

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

Задача IoC-фреймворка вообще не в том чтобы у вас появился DI, задача фреймворка - облегчить конструирвование цепочек объектов, когда вы уже обмазались DI. Фреймворк - это вспомогательный инструмент, уменьшающий количество механической работы когда вы уже вступили на путь "хочу инжектировать объекты туда сюда".

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

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

Но ведь с зависимостями вы так же их где-то конфигурируете и потом таскаете по кодовой базе, просто для вас это выглядит так как будто зависимость появилась в месте использования без засорения кода. В FastAPI пробрасывание того же конфига через Depends() код тоже не засоряет. Вы читаете конфиг в одном месте, например on_startup, делаете вокруг него зависимость в терминах FastAPI и добавляете её там где надо и получаете доступ к конфигу в нужном месте. Так же и с конфигурируемыми объектами, БД и т. д.

На самом деле зависимости в FastAPI - это как фикстуры в pytest, наверное, самая близкая аналогия.

Без контейнера, как показывает практика, в большинстве случае это делают везде, кто где захотел - часто такое можно увидеть прямо внутри __init__

А с контейнером, во-первых, все находится в одном месте - это уже проще найти в коде, также появляется отдельная зона ответственности, которая занимается сборкой объектов. Во-вторых - таскать за собой нужно только маркеры и декоратор типа inject, но я не особо понимаю в чем здесь проблема

В своем фреймворке я сделал автоинъекцию по типу объекта, в этом случае вам бы понадобился только условный декоратор @autoinject

P.S. Вы упомянули проброс конфига с Depends. Если вы инжектите конфиги в хендлеры, на мой взгляд, это уже звоночек, что что-то делается не так, если нет обоснования так делать

На самом деле зависимости в FastAPI - это как фикстуры в pytest, наверное, самая близкая аналогия.

И да и нет. pytest - как раз пример хорошего, но специализированного IoC конетйнера. В нем есть несколько скоупов (что-то живет всё время, что-то для группы тестов, что-то на время одного теста), изоляция неймспейсов при поиске фикстур (фикстуры одного модуля не мешают другому), отложенное связывание (вы используете имя фикстуры, а не ссылку на неё). В фастапи этого практически нет, а dishka больше похож - есть и вложенные скоупы и изоляция с помощью выделения компонентов, хотя ввиду специфики, устроено чуточку по другому. Но соглашусь, всё это контейнеры с чуть разными возможностями.

чел, ты хорош и сложности с DI в fastapi довольно точно подметил.

но у тебя в статье:

> Так вот: помним, что в DI нам нужно завязывать на абстракцию

Вот это откуда? этого ведь ни в торе, ни конституции ни в здравом смысле нет.
Зачем смешивать DI и всякие IoCи и прочие "зависим от абстракции"?

DI сам по себе вполне справляется как в сочетании с абстрациями, так и без - миллион юзеров spring в джаве инжектящих свои классы не дадут соврать.

Согласен, тут имеет место быть небольшое смешение понятий. Для меня DI неразрывен от DIP, т.к. без него он приносит только половину пользы. Вторую половину я и хотел показать в статье.

Ну правда в том, что такое нужно, если подмена инжектируемого объекта нужна в продакшен коде (например, когда у тебя приложение рассчитывает уметь запускаться на разных базах - и речь не про "когда-то в будущем поменяем базу" - тут как раз снова становится бессмысленно - в будещем старую имлементацию удалишь, новую заюзаешь)

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

В итоге все эти TokenRepo так и остаются интерфейсами/протоколами с одной имплементацией:

class TokenRepo(Protocol):
    async def get_user_by_token(self, token: str) -> str | None:
        ...

    async def set_user_token(self, token: str, username: str) -> None:
        ...


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

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

Никто не планирует сроки поддержки библиотеки. Поэтому и нужно "сообщество" и круг мейнтейнеров, а не один человек. Bus factor никто не отменял. В жзни всякое может случиться и мейнтейнер может пропасть.
Яркие примеры - rocketry, faust. Но тот же Faust подхватило "сообщество", сделало форк и развивает дальше.
В этом у dishka тоже есть плюс, т.к. он входит в небольшую, но все-таки организацию энтузиастов, над проектом работает сразу несколько вовлеченных контрибуторов, и флаг есть кому подхватить

Спасибо за статью о dishka и разбор fastapi.

  1. Первую половину статьи вы "ругаете" fastapi за то, что он неправильный DI(dependency inversion) так как зависим от реализации, но кто сказал, что Depends про это? Depends это про DI(dependency injection), где суть во внедрении зависимостей снаружи, а не создание их внутри объектов(композиция -> агрегация). В самой документации fastapi о depends не встретил упоминания об инверсии зависимостей.

  2. Вы говорите, что dishka поможет нам внедрять(инвертировать) зависимости на разных уровнях и не только в контроллере. Правильно ли я понимаю, что у меня весь инфраструктурный код реализаций абстракций будет "усеян" этим декоратором и весь проект будет сильно привязан к этой библиотеке? В то время как с Depends мы собираем зависимости в рамках контроллера и детали реализации просто ждут снаружи требуемые им параметры?

Depends это про внедрение, а Dishka ещё и про инверсию и не только на уровне контроллера. Сравнивать их напрямую, кажется не совсем правильно. Это тот самый случай, когда DI != DI )))

Отвечу на второй вопрос. inject - эта декораторы созданные под конкретные фреймворки, это интеграции, которая предоставляет dishka (в данной статье интеграция с fastapi) и используется для удобного внедрения зависимостей в контроллер. В остальном же резолвинг зависимостей настраивается в специальных классах провайдерах. Сами зависимости ничего не знают о библиотеке.
Приведу пример на псевдокоде:
Есть достаточно типичная ситуация, когда мы вызываем в контроллере интерактор (use case), который в свою очередь зависит от репозитория, а наша реализация репозитория зависит от сессии базы данных.

В таком случае в контролере мы укажем с помощью аннотации FromDishka, что хотим получить интрактор и используем декоратор inject (на самом деле это не обязательно, для многих интеграций есть функция autoinject). И просто вызовем у него соответствующий метод.

@router.get("/command")
@inject
def my_controller(interactor: FromDishka[MyInteractor]):
  interactor.execute()

Код нашего интерактора и репозитория будут выглядеть так:

class Repo(Protocol):
  def get_user(self) -> User: ...

class MyInteractor:
  def __init__(self, repo: Repo):
    self._repo = repo

  def execute(self):
    # тут код использующий репозиторий

А наша реализация репозитория:

class MyRepo(Repo):
  def __init__(self, session: Session):
    self._session = session

  def get_user(self) -> User: ...
    # конкретная реализация

Обратите внимания ни реализация репозитория, ни интерактор ничего не знают о дишке

Далее мы напишем провайдер, в котором опишем, то какие зависимости должны подставляться:

class MyProvider(Provider):
  scope = Scope.REQUEST

  @provide
  async get_session(self) -> AsyncIterator[Session]:
    session = make_session()
    try:
      yield session
    finally:
      await session.close()

  my_repo = provide(MyRepo, provides=Repo)
  my_interactor = provide(MyInteracotor)


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

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

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


Нет, dishka inject - это декоратор под конкретный фреймворк и работает только в контроллере. Во всех остальных частях кода вам остается таскать контейнер явным образом и делать "container.get(T)" для получения зависимостей. Или писать свой inject-декоратор. Однако, это не рекомендуется делать, о чем я в статье вскольз предупреждаю.

Fun Fact: @ в блоке кода всё равно тегает юзера.

Sign up to leave a comment.

Articles