В свое время 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, а это позволяет нам
Заменить Redis на Memcached или даже хранение в памяти при необходимости
Поместить в качестве 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
Концепция проста и незамысловата:
Мы пишем "провайдеры" - классы, которые содержат в себе фабрики зависимостей
Затем объединяем их в "контейнер", откуда они уже будут доставляться в конечные функции
Используем интеграции с фреймворками для бесшовного встраивания контейнера в ваше приложение
Пишем "провайдер"
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 (как в моем примере) и/или lifespanrequest.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).
Лично я рекомендую вам как минимум обратить внимение на эту библиотеку, а еще:
поддержать ее автора, поставив звезду на GitHub
вступить в телеграм чат, где вы можете пообщаться с ее создателем лично
прочитать статью про использование dishka с Litestar и FastStream
посмотреть доклад автора dishka на Podlodka Python Crew
подписаться на мой телеграм канал, если вам интересен подобный материал
Комментарии (31)
Mingun
16.12.2024 14:51Скажите, а кто-нибудь пробовал завести этот
FastApi
под msys2? Решил попробовать его под новый пет-проект для построения несложного API вместо привычного в этих случаях php, но сразу же столкнулся с тем, что он не хочет устанавливаться. При попытке установки через pip зачем-то пытается собрать какую-то зависимость из исходников (какой-то maturin, ХЗ, что такое). Потом после гугления обновил пакеты и нашёл, что есть пакетmingw-w64-x86_64-python-fastapi
, который вроде бы то, что нужно, но поставляемый с ним инструмент CLI fastapi при запуске сразу же падает, ругаясь на то, что нет модуляfastapi_cli.cli
(и его действительно нет, пакет эти каталоги не ставит!) и отправляя опять в pip. Круг замкнулся и не могу это победить уже второй день.Tishka17
16.12.2024 14:51Не очень понятно, что вы делаете с msys2. Ваша целевая платформа для сервера windows?
Mingun
16.12.2024 14:51Ставлю его под msys2. Просто у меня Windows на рабочей машине и издревле я использую msys2 и утилиты из него для всякого рода автоматизации. Скрипты там разные и так далее.
Propan671 Автор
16.12.2024 14:51Используйте запуск через
uvicorn
или любой другой ASGI сервер вместо FastAPI CLIMingun
16.12.2024 14:51А поподробнее можно? Что значит запуск через
uvicorn
?uvicorn
и так среди зависимостей ставился, что ещё надо?brotchen
16.12.2024 14:51Документация в разделе Run the Server Program предлагает такой способ:
uvicorn main:app --host 0.0.0.0 --port 80
ammo
16.12.2024 14:51Забавно выходит, вы же автор fastdepends, но при этом рекомендуете dishka, а не свой проект.
Propan671 Автор
16.12.2024 14:51Ну, то, что он мой - не значит, что он самый-самый). FastDepends - это буквально копия API FastAPI и он грешит теми же проблемами в качестве DI
Но он решает несколько другую проблему, нежели просто DI и нужен он мне именно в таком виде. Все-таки в качестве чистого IoC контейнера dishka значительно лучше.
Ryav
16.12.2024 14:51Позволяет переиспользовать контейнер зависимостей в рамках всего приложения (и других фреймворков/библиотек), а не только
handler
'ах FastAPI (аккуратнее с этим).А дайте пример, как мне на слое сервисов достать зависимость, например, сессию к БД. Сейчас приходится её прокидывать от handler'а глубже, хотя в handler'е она точно не нужна.
Tishka17
16.12.2024 14:51Так не достать, а передать. =) Прям в инит сервиса/интерактора передать условный DAO. Соответсвтенно в хэндлере никаких сесиий, только инстанс сервиса, а контейнер уже всё связывает друг с другом. Пример есть в документации https://dishka.readthedocs.io/en/stable/quickstart.html
nightblure
16.12.2024 14:51Спасибо за статью
Рекомендую посмотреть https://github.com/nightblure/injection. Более легкая версия dependency-injector, не перегруженная лишним. Еще есть понятный механизм финализации ресурсов и удобное переопределение зависимостей как в dependency-injector
Tishka17
16.12.2024 14:51Насколько я помню, в dependency injector финализация как раз сделана очень ограниченно - мы можем финализировать только синглтоны (через Resource), либо при использовании inject забыв про финализацию транзитивных зависимостей.
На мой взгляд, dependency-injector - это очень неудачное решение для контейнера не потому что разработчики перестали его поддерживать, а с точки зрения дизайна API. То есть все клоны будут обраладть теми же пробелмами: слабый контроль жизненного цикла объектов, необходимость ссылаться на конкретный класс контейнере там где мы хотим просто иметь зависимость, неявная передача контейнера через глобальные переменные.
Посмотрите внимательнее на dishka, тут все работает чуточку по другому и я верю, что оно дает больше возможностей и контроля, может и вам зайдет
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)Tishka17
16.12.2024 14:51Ну вот function-scoped зависимости финализировать как раз самое важное на мой взгляд. Приложение может никогда не перезапускаться или умирать только под SIGKILL, а всякие соединения, скрытые за абстракциями, возвращать в луп приходится регулярно.
Про громоздкость API грустно слышать, потому что количество разных сущностей на порядок меньше чем в dependency-injector. Знай дергай себе provide с указанием скоупа, а потом тот же inject. В том время как в dependency-injector десятка два видов фабрик и даже не знаешь какую когда юзать. Возможно дело в сложном квикстарте, если есть советы как улучшить - буду рад услышать
iroln
16.12.2024 14:51Как видите, приседаний стало меньше
А кажется не стало. Если вам реально нужен DI в вашем приложении, нужно менять реализации в зависимости от среды или фазы луны, наверное всё это нагромождение абстракций имеет какой-то смысл. Но чаще всё это просто не нужно. Зачастую это переусложнение ради "а вдруг когда-нибудь пригодится", потому что умному программисту Васе стало скучно. Например, заморачиваться с "настоящим" DI только ради тестов с моками - это очередное "безумие индустрии". Ведь всё это дополнительный код, дополнительная когнитивная нагрузка, дополнительная поддержка. Надёжнее и правильнее выделить специальное окружение и развернуть в нём всё ваше барахло для интеграционного тестирования, максимально приближенного к жизни.
FastAPI задуман простым, понятным и элегантным, с минимумом бойлерплейта и без нагромождения абстракций и паттернов из мира Java. И этим он снискал такую славу. Люди в конце концов любят простоту. Даже тот, кто когда-то писал сложный и запутанный код, с опытом возвращается к очень простым концепциям.
В одном продукте у нас было примерно 100 KLOC в сервисах на FastAPI и ни разу не понадобилось городить там полноценный DI. Может быть мы что-то делали неправильно, но всё работало, и всё было удобно.
В общем, я бы десять раз подумал прежде чем обмазываться сложными абстракциями, чтобы было "правильно" и по-взрослому. Возможно, вам всё это на самом деле не нужно.
Tishka17
16.12.2024 14:51DI это не только про смену реализаций, это ещё и про управление жизненным циклом и умение пробросить параметризованные объекты без протаскивания этих параметров по всем слоям.
Что же касается 2 реализаций, они часто есть - для теста и для прода. Вопрос не про сложные абстракции, вопрос про отделение контрактов от деталей, когда приложение растет, это становится полезным.
В защиту dishka скажу, что с ним вы не привязаны к fastapi и можете делать какие-то вещи, с которыми у фастапи есть проблемы. То есть это универсальное решение, которое вы можете использовать с любым фреймворком (нам же не всегда fastapi хватает, иногда и grpc хочется)
nightblure
16.12.2024 14:51Но дело ведь не только в "менять реализации в зависимости от среды или фазы луны". Лично я вижу две главные цели таких фреймворков:
1. Позволить нормально внедрять зависимости вместо повсеместного таскания всяких конфигов, json-ов, yaml-ов, переменных окружения и прочего. Таскать это по всей кодовой базе - значит нарушать зоны ответственности и усложнять код
2. Облегчить тестирование. Тесты становится писать в удовольствие, когда вместо манкипатчинга в каждом тесте можно просто вызвать метод типа override, который подменит объект везде, где использовался связанный провайдер
По поводу абстракций: как и в любой пакет/любую библиотеку появляется некий накладной расход в виде того, что нужно почитать доку, попробовать в использовании и т.д. - без этого никак. Но фреймворков уже немало и у вас есть выбор - в каком-то из них вам будет явно проще освоиться, чем в другомTishka17
16.12.2024 14:51Задача IoC-фреймворка вообще не в том чтобы у вас появился DI, задача фреймворка - облегчить конструирвование цепочек объектов, когда вы уже обмазались DI. Фреймворк - это вспомогательный инструмент, уменьшающий количество механической работы когда вы уже вступили на путь "хочу инжектировать объекты туда сюда".
То есть, мы изначально решаем использовать внедрение зависимостей для каких-то целей - подмены реализации, конфигурирование объектов, скрытие тразитивных зависимостей от места использования, управление жизненным циклом. Дальше это логично отделяется от основной логики - мы получаем самописный контейнер (а кто-то получает сервис локатор, что про другое но на самом деле связано). И вот тут мы уже можем неплохо автоматизировать его и заюзать фреймворк.
iroln
16.12.2024 14:51Позволить нормально внедрять зависимости вместо повсеместного таскания всяких конфигов, json-ов, yaml-ов, переменных окружения и прочего. Таскать это по всей кодовой базе - значит нарушать зоны ответственности и усложнять код
Но ведь с зависимостями вы так же их где-то конфигурируете и потом таскаете по кодовой базе, просто для вас это выглядит так как будто зависимость появилась в месте использования без засорения кода. В FastAPI пробрасывание того же конфига через
Depends()
код тоже не засоряет. Вы читаете конфиг в одном месте, напримерon_startup
, делаете вокруг него зависимость в терминах FastAPI и добавляете её там где надо и получаете доступ к конфигу в нужном месте. Так же и с конфигурируемыми объектами, БД и т. д.На самом деле зависимости в FastAPI - это как фикстуры в pytest, наверное, самая близкая аналогия.
nightblure
16.12.2024 14:51Без контейнера, как показывает практика, в большинстве случае это делают везде, кто где захотел - часто такое можно увидеть прямо внутри
__init__
А с контейнером, во-первых, все находится в одном месте - это уже проще найти в коде, также появляется отдельная зона ответственности, которая занимается сборкой объектов. Во-вторых - таскать за собой нужно только маркеры и декоратор типа inject, но я не особо понимаю в чем здесь проблема
В своем фреймворке я сделал автоинъекцию по типу объекта, в этом случае вам бы понадобился только условный декоратор@autoinject
P.S. Вы упомянули проброс конфига с Depends. Если вы инжектите конфиги в хендлеры, на мой взгляд, это уже звоночек, что что-то делается не так, если нет обоснования так делать
Tishka17
16.12.2024 14:51На самом деле зависимости в FastAPI - это как фикстуры в pytest, наверное, самая близкая аналогия.
И да и нет. pytest - как раз пример хорошего, но специализированного IoC конетйнера. В нем есть несколько скоупов (что-то живет всё время, что-то для группы тестов, что-то на время одного теста), изоляция неймспейсов при поиске фикстур (фикстуры одного модуля не мешают другому), отложенное связывание (вы используете имя фикстуры, а не ссылку на неё). В фастапи этого практически нет, а dishka больше похож - есть и вложенные скоупы и изоляция с помощью выделения компонентов, хотя ввиду специфики, устроено чуточку по другому. Но соглашусь, всё это контейнеры с чуть разными возможностями.
bokshi
16.12.2024 14:51чел, ты хорош и сложности с DI в fastapi довольно точно подметил.
но у тебя в статье:
> Так вот: помним, что в DI нам нужно завязывать на абстракцию
Вот это откуда? этого ведь ни в торе, ни конституции ни в здравом смысле нет.
Зачем смешивать DI и всякие IoCи и прочие "зависим от абстракции"?DI сам по себе вполне справляется как в сочетании с абстрациями, так и без - миллион юзеров spring в джаве инжектящих свои классы не дадут соврать.
Propan671 Автор
16.12.2024 14:51Согласен, тут имеет место быть небольшое смешение понятий. Для меня DI неразрывен от DIP, т.к. без него он приносит только половину пользы. Вторую половину я и хотел показать в статье.
healfy
16.12.2024 14:51У меня единственный вопрос, когда вы пишите эти библиотеки, как долго вы планируете их поддерживать? Потому что на мой взгляд самое плохое в таких вот тулзах, это то что со временем они перестают быть нужными их авторам, и depency injector яркий пример.
Propan671 Автор
16.12.2024 14:51Никто не планирует сроки поддержки библиотеки. Поэтому и нужно "сообщество" и круг мейнтейнеров, а не один человек. Bus factor никто не отменял. В жзни всякое может случиться и мейнтейнер может пропасть.
Яркие примеры - rocketry, faust. Но тот же Faust подхватило "сообщество", сделало форк и развивает дальше.
В этом у dishka тоже есть плюс, т.к. он входит в небольшую, но все-таки организацию энтузиастов, над проектом работает сразу несколько вовлеченных контрибуторов, и флаг есть кому подхватить
qqqoid
>> Наверное, первое, что приходит на ум - это Dependency Injector. Но, кажется, создатель отказался от его сопровождения (да и библиотека имеет множество минусов, которые нам тоже не нравятся).
Это не так, DI вполне себе развивается, проверьте гитхаб. На вкус и цвет все фломастеры разные
Propan671 Автор
Ну ничего себе, действительно ожил. Однако, 1 коммит раз в месяц - это не прям "активное развитие". На самом деле, я не против Dependency Injector, просто он очень сильно перегружен, реализация на глобалах и вроде как не thread-safe
qqqoid
Да, одно время были мысли отказываться, некоторое время жили со своим форком, но он ожил в ноябре ещё и недавно довольно серьезно бампнул накопившееся, на данный момент авторский в полностью адекватном времени состоянии. Не так уж и один, свежих коммитов около десятка, но где-то там в глубинах имеющийся рассказ о том, что автор отошёл от питонячьих дел и в основном девопсит некоторую настороженность зародили давно уже.
Tishka17
Насчет вкуса конечно верно, но к разработке своего контейнра мы пришли с несколькими требованиями. В частности:
* контейнер должен уметь финализировать объекты после использования и появление таких промежуточных объектов не должно менять логику работы с целеым
* контейнер должен уметь выдавать одни и те же объекты если их запрашиваю в пределах одного "скоупа"
С обоими пунктами у dependency-injector все очень странно. То есть мы можем использовать его декоратор `@inject`, но при этом придется вручную перечислять в функции, что мы финалиизруем, хотя это вообще могут быть транзиитвные завимисоти. Как он переиспользует объекты тоже не оченвидно, так как понятия скоупа фактически нет за пределами этого декоратора.
qqqoid
Не пользуемся инжектом, всё собираем в контейнерах, так оказалось ближе
qqqoid
Я обязательно попробую ваш вариант и звёздочку поставил, мне кажется уже встречал его в метаниях в какой-то момент. Тревожный звоночек был уже, что проект встанет на ручнике