В свое время FastAPI прогремел как гром среди ясного неба - тут тебе и минималистичный API аля-Flask (все устали от Django, диктующего свои правила), и OpenAPI документация из коробки, и удобное тестирование, и хайповая асинхронность. Буквально все, что нужно для свободы творчества, и никаких ограничений! Да еще и Depends завезли! В тот момент это был культурный шок - Dependency Injection в Python? Разве это не что-то из Java?

FastAPI показал, что DI - это паттерн, упрощающий разработку вне зависимости от языка программирования. Теперь DI как фича является практически неотъемлемым элементом любого нового Python-фреймворка (Litestar/Blacksheep/FastStream/etc), ведь людям это нужно. Все хотят "как в FastAPI".

Но дьявол кроется в деталях. А вы уверены, что те самые Depends == Dependency Injection? Уверены, что пишете код на FastAPI правильно?

Что Tiangolo (создатель FastAPI) прививает вам "лучшие практики"?

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

Что такое DI и зачем он нам нужен?

Dependency Injection - это паттерн, сильно помогающий следовать принципу Инверсии зависимостей (DIP - Dependency Inversion Principle) из soliD.

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

А вот с помощью паттерна Dependency Injection "реальные" имплементации этих интерфейсов (которые знают про конкретные базы данных и тд) будут доставляться в вашу логику извне при инициализации проекта (тот самый Injection).

Т.е. вместо подобного кода:

class TokenChecker:
    def __init__(self) -> None:
        self.storage = Redis()

    def check_token(self, token: str) -> bool:
        return self.storage.get(token) is not None

checker = TokenChecker()

Мы должны писать нечто такое:

from typing import Protocol

# находится в БЛ, так как нужен для ее функционирования
class Storage(Protocol):
    def get(self, token: str) -> str | None:
        ...

class TokenChecker:
    def __init__(self, storage: Storage) -> None:
        self.storage = storage

    def check_token(self, token: str) -> bool:
        return self.storage.get(token) is not None

real_storage = Redis() # объект Redis подходит под интерфейс Storage
checker = TokenChecker(real_storage)

Кода стало больше, но зачем? - Теперь TokenChecker больше не знает о том, что работает с Redis, а это позволяет нам

  1. Заменить Redis на Memcached или даже хранение в памяти при необходимости

  2. Поместить в качестве Storage mock-объект в тестах удобным и понятным способом

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

Однако, DI - это хорошая практика, которая приносит ощутимую пользу в любых языках.

Иногда вы можете встретить еще и формулировку Inversion of Control (IoC), что суть - о том же самом. Когда мы следуем подходу Dependency Injection, у нас образуется отдельная группа функций и классов, выполняющих только одну задачу - создание других объектов.
В сложном приложении такой компонент может содержать большое количество функций, контролировать как создание, так и корректную очистку объектов и, что самое главное - их взаимосвязь. Для упрощения работы с такими фабриками придумали отдельный тип библиотек - IoC-контейнеры (DI-фреймворки).

DI в FastAPI по Tiangolo

Одна из основных фич FastAPI - его Depends, которая как раз позиционируется как реализация Dependency Injection принципа. Давайте посмотрим, как Tiangolo предлагает ее использовать:

from typing import Annotated

from fastapi import Depends

async def common_parameters(
    q: str | None = None,
    skip: int = 0,
    limit: int = 100,
):
    return { "q": q, "skip": skip, "limit": limit }

@app.get("/items")
async def read_items(
    commons: Annotated[dict, Depends(common_parameters)],
):
    return commons

@app.get("/users")
async def read_users(
    commons: Annotated[dict, Depends(common_parameters)],
):
    return commons

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

"Так вы можете переиспользовать логику между разными эндпоинтами" - вот как аргументирует использование Depends Tiangolo. Однако, это не Dependency Injection.

Просто давайте взглянем на следующий код:

from typing import Annotated

from fastapi import Request

async def common_parameters(
    q: str | None = None,
    skip: int = 0,
    limit: int = 100,
):
    return { "q": q, "skip": skip, "limit": limit }

@app.get("/items")
async def read_items(request: Request):
    commons = await common_parameters(**request.query_params)
    return commons

@app.get("/users")
async def read_users(request: Request):
    commons = await common_parameters(**request.query_params)
    return commons

Разве это не то же самое "переиспользование логики", с которым нам хочет помочь Tiangolo? Кажется, его помощь - это просто еще один слой синтаксического сахара (не бесплатного, конечно).

Однако, Dependency Injection тут все-таки есть, т.к. есть возможность заменить зависимость через механизм dependency-overrides

async def override_dependency(q: str | None = None):
    return {"q": q, "skip": 5, "limit": 10}

app.dependency_overrides[common_parameters] = override_dependency

В данном случае мы подменяем все "зависимости" вида Depends(common_parameters) на Depends(override_dependency) по всему проекту. Т.е., когда запрос придет в обработчик, вместо оригинальной функции common_parameters будет вызвана override_dependency вне зависимости от сигнатуры самого обработчика.

В варианте с прямым использованием функции это невозможно.

Правда, механизм позиционируется "для тестов" и все еще не помогает соблюсти DIP - мы подменяем зависимость от реализации на другую зависимость от реализации. Что может только путать людей, работающих с кодовой базой.

Но не все потеряно и мы можем доработать Depends так, чтобы это был настоящий DI с соблюдением DIP.

"Настоящий" DI в FastAPI

Не претендую на авторство данного подхода, но готов принять все шишки за его использование, т.к. не нашел способа сделать DI лучше.

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

В FastAPI МОЖНО реализовать Dependency Injection с соблюдением DIP. Но не совсем тем способом, которым планировал Tiangolo.

В FastAPI у нас есть глобальный словарь app.dependency_overrides, который предлагается использовать для "тестирования зависимостей" (в документации). Однако, по всем внешним признакам - это контейнер зависимостей. И мы можем его использовать как раз по прямому назначению IoC контейнера - Inject'ить зависимости.

Давайте разбираться по порядку.

Вводим абстракцию

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

Для начала введем интерфейс объекта, с помощью которого мы как раз будем валидировать токен:

from typing import Protocol

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:
        ...

Зависим от абстракции

Теперь нам нужно "завязаться" на эту абстракцию в нашем эндпоинте:

from typing import Annotated
from fastapi import FastAPI, Depends

app = FastAPI()

@app.get("/{token}")
async def get_user_by_token(
    token: str,
    token_repo: Annotated[TokenRepo, Depends()],  # "запрашиваем" абстракцию
) -> str | None:
    return await token_repo.get_user_by_token(token)

Пишем реализацию

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

Реализация протокола для работы с Redis:

from redis.asyncio import Redis

class RedisTokenRepo(TokenRepo):
    def __init__(
        self,
        redis: Redis,
        expiration: str,
    ) -> None:
        self.redis = redis
        self.token_expiration = expiration

    async def get_user_by_token(self, token: str) -> str | None:
        if username := await self.redis.get(token):
            return username.decode()

    async def set_user_token(self, token: str, username: str) -> None:
        await self.redis.set(
            name=token,
            value=username,
            ex=self.token_expiration,
        )

Используем реализацию вместо абстракции

Ну и "помещаем" нашу реализацию в FastAPI IoC Container:

def setup_ioc_container(
    app: FastAPI,
) -> FastAPI:
    settings_object = {  # mock настроек
        "redis_url": "redis://localhost:6379",
        "token_expiration": 300,
    }

    redis_repo = RedisTokenRepo(
        redis=Redis.from_url(settings_object["redis_url"]),
        expiration=settings_object["token_expiration"],
    )

    app.dependency_overrides.update({
        TokenRepo: lambda: redis_repo,
    })

    return app

Реальной зависимостью в нашем случае является lambda: redis_repo. Именно эта функция будет вызываться при каждом запросе с Annotated[TokenRepo, Depends()] зависимостью.

Мы реализовали ее через lambda для того, чтобы избежать вызова конструктора RedisTokenRepo на каждый вызов, а сделать этот объект "синглтоном".

Так выглядит DI в FastAPI "здорового человека". Но не совсем.

Боремся с FastAPI

К сожалению, Tiangolo не планировал использование Depends таким образом. Он не хочет, чтобы мы зависели от "абстракции". Поэтому в нашу OpenAPI схему просочилось что-то странное (args, kwargs?):

Это происходит потому что FastAPI парсит сигнатуру "зависимости", которую мы запрашиваем (Annotated[TokenRepo, Depends()]), а именно - __init__ метод класса.

class TokenRepo(Protocol):
     # init класса Protocol по умолчанию содержит args, kwargs
     def __init__(self, *args, **kwargs): ...

Вот FastAPI и нашел "лишние" аргументы и нарисовал их в сигнатуре.

Для того, чтобы от этого избавить нужно "спрятать" от FastAPI сигнатуру исходной "абстракции". (Можно еще отнаследоваться от abc.ABC вместо typing.Protocol, но это уже "протекание" деталей FastAPI в наши абстракции, чего мы не хотим)

Сделать это можно следующим образом:

from typing import Callable, Any

class Stub:
    def __init__(self, dependency: Callable[..., Any]) -> None:
        """Сохраняем нашу абстракцию."""
        self._dependency = dependency

    def __call__(self) -> None:
        """Выкинем ошибку, если забыли подменить реализацию при старте приложения."""
        raise NotImplementedError(f"You forgot to register `{self._dependency}` implementation.")

    def __hash__(self) -> int:
        """Обманываем app.dependency_overrides, чтобы он считал Stub реальной зависимостью"""
        return hash(self._dependency)

    def __eq__(self, __value: object) -> bool:
        """Обманываем app.dependency_overrides, чтобы он считал Stub реальной зависимостью"""
        if isinstance(__value, Stub):
            return self._dependency == __value._dependency
        else:
            return self._dependency == __value

Теперь мы должны "запрашивать" зависимость следующим образом:

@app.get("/{token}")
async def get_user_by_token(
    token: str,
    token_repo: Annotated[TokenRepo, Depends(Stub(TokenRepo))]
): ...

Уже не так "сахарно", зато в схему ничего не течет.

Резюме

Dependency Injection в FastAPI возможен, однако:

  • требует дополнительных приседаний, чтобы ничего не утекало в схему

  • требует дополнительных приседаний для реализации Application-level зависимостей (объктов, которые создаются 1 раз при старте приложения)

  • требует дополнительных приседаний для регистрации зависимостей

Альтернатива?

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

А что нам остается?

Все как-то не то. Слабое распространение, скудный функционал, нестабильный API.

В общем, в ходе продолжительных баталий дискуссий опытный разработчик Андрей Тихонов (автор канала Советы разработчикам, администратор ru-python, fastapi-ru и прочих крупных TG-групп) решил создать собственное решение - dishka!

Полное сравнение с другими библиотеками вы можете найти в документации библиотеки.

Но я пределагаю сначала взглянуть, как он работает, а потом уже сравнить с FastAPI Depends.

Использование dishka

Концепция проста и незамысловата:

  1. Мы пишем "провайдеры" - классы, которые содержат в себе фабрики зависимостей

  2. Затем объединяем их в "контейнер", откуда они уже будут доставляться в конечные функции

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

Пишем "провайдер"

from dataclasses import dataclass

from dishka import Provider, Scope, provide, from_context

@dataclass
class Settings:
    redis_url: str
    token_expiration: int

class RepoProvider(Provider):
    # говорим, что объект типа Settings будем помещаться в контейнер пользователем
    settings = from_context(provides=Settings, scope=Scope.APP)

    @provide(scope=Scope.APP)  # зависимость уровня приложения (синглтон)
    def get_redis_token_repo(
        self,
        settings: Settings,  # "запрашиваем" другую зависимость
    ) -> TokenRepo:
        return RedisTokenRepo(
            redis=Redis.from_url(settings.redis_url),
            expiration=settings.token_expiration,
        )

Затем мы должны собрать из провайдера (у нас он один) контейнер

container = make_async_container(
    RepoProvider(),
    context={  # помещаем Settings в контейнер вручную
        Settings: Settings(
            redis_url="redis://localhost:6379",
            token_expiration=300,
        ),
    },
)

И наконец - используем этот контейнер в нашем FastAPI приложении!

from fastapi import APIRouter, FastAPI
from dishka.integrations.fastapi import FromDishka, DishkaRoute, setup_dishka

router = APIRouter(route_class=DishkaRoute)  # используем специальный route_class

@router.get("/{token}")
async def get_user_by_token(
    token: str,
    token_repo: FromDishka[TokenRepo],  # используем вместо Depends
) -> str | None:
    return await token_repo.get_user_by_token(token)

app = FastAPI()
app.include_router(router)
setup_dishka(container, app)

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

Выводы по dishka

По моему субъективному мнению dishka значительно комфортнее для реализации DI принципа в FastAPI проекте (относительно нативного Depends) по следуюшим причинам:

  • Имеет четкое разделение на Application-level (синглтоны в рамках приложения) и Request-level (создаются на каждый запрос) зависимости. Нативный Depends работает только для Request зависимостей, а Application-level (самые частые) приходится изобретать самостоятельно вокруг main (как в моем примере) и/или lifespan request.state.* (как советует Starlette). Также dishka поддерживает и другие Scope'ы, в т.ч. и кастомные, что позволяет использовать его в совершенно разных кейсах.

  • Финализация Application-level зависимостей. В FastAPI отдельной головной болью стоит вопрос о том, как их финализировать, а для асинхронных зависимостей - еще и инициализировать (асинхронный main? извращения с lifespan?). Dishka поддерживает как асинхронные фабрики зависимостей, так и фабрики с yield, так что обе проблемы для него просто не существуют.

  • Помогает организовать логику управления графом зависимостей в одном месте, не размазывая ее по разным функциям и частям приложения (а также избавляет от различных служебных функций-оберток, необходимых для победы над FastAPI)

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

  • Работает несколько быстрее стандартного Depends

Однако, у использования dishka есть и свои минусы

  • Придется потратить 20 минут на изучение новой библиотеки

  • +1 зависимость (и библиотека в вашем портфолио)

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

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

  • Нет возможности учитывать зависимости при генерации OpenAPI

Выводы

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

В данной статье мы рассмотрели самый популярный и "правильный" подход к реализации принципа внедрения зависимостей в рамках FastAPI приложения, а также познакомились с dishka - великолепной библиотекой, которая позволяет реализовать DI в рамках любого приложения (в т.ч. и FastAPI).

Лично я рекомендую вам как минимум обратить внимение на эту библиотеку, а еще:

Комментарии (31)


  1. qqqoid
    16.12.2024 14:51

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

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


    1. Propan671 Автор
      16.12.2024 14:51

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


      1. qqqoid
        16.12.2024 14:51

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


    1. Tishka17
      16.12.2024 14:51

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

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


      1. qqqoid
        16.12.2024 14:51

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


      1. qqqoid
        16.12.2024 14:51

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


  1. Mingun
    16.12.2024 14:51

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


    1. Tishka17
      16.12.2024 14:51

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


      1. Mingun
        16.12.2024 14:51

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


    1. Propan671 Автор
      16.12.2024 14:51

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


      1. Mingun
        16.12.2024 14:51

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


        1. brotchen
          16.12.2024 14:51

          Документация в разделе Run the Server Program предлагает такой способ:

          uvicorn main:app --host 0.0.0.0 --port 80


  1. ammo
    16.12.2024 14:51

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


    1. Propan671 Автор
      16.12.2024 14:51

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

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


  1. Ryav
    16.12.2024 14:51

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

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


    1. Tishka17
      16.12.2024 14:51

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


  1. nightblure
    16.12.2024 14:51

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

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


    1. Tishka17
      16.12.2024 14:51

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

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

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


      1. nightblure
        16.12.2024 14:51

        Финализация с 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)


        1. Tishka17
          16.12.2024 14:51

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

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


  1. iroln
    16.12.2024 14:51

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

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

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

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

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


    1. Tishka17
      16.12.2024 14:51

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

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

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


    1. nightblure
      16.12.2024 14:51

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

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

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

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


      1. Tishka17
        16.12.2024 14:51

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

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


      1. iroln
        16.12.2024 14:51

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

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

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


        1. nightblure
          16.12.2024 14:51

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

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

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

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


        1. Tishka17
          16.12.2024 14:51

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

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


  1. bokshi
    16.12.2024 14:51

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

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

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

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

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


    1. Propan671 Автор
      16.12.2024 14:51

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


  1. healfy
    16.12.2024 14:51

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


    1. Propan671 Автор
      16.12.2024 14:51

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