Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Нужен ли нам в python DI и какой?

Denis Anikin
September 29, 2024

Нужен ли нам в python DI и какой?

В своем докладе я расскажу о DI фреймворках и о том, нужны ли они в вашей кодовой базе. Я свяжу это с SOLID, постараюсь наглядно продемонстрировать почему DI может быть полезен и постараюсь поговорить о скорости DI фреймворков. Конечно же, поговорим и о скорости работы DI.

Denis Anikin

September 29, 2024
Tweet

More Decks by Denis Anikin

Other Decks in Programming

Transcript

  1. Что я такое? — Я техлид/комьюнити лид — fullstack, python,

    typescript, devops, микросервисы, kubernetes — Выступаю на конференциях — Отвечаю за внутреннее сообщество питонистов https://xfenix.ru Кто я?
  2. — «Слушай, всё уже было в питоне с 2013, а

    то и ранее» — «Всё уже сказано, смысл какой обсуждать?» — «Всё понятно» — «Это и дураку ясно» Про DI люди говорят
  3. — DI в питоне популярен всего несколько лет — Фреймворки

    растут по сей день — Тема DI и принципа DIP из SOLID — сложные — Когда мы сюда добавляем ещё и DI фреймворк и IoC — все становится сложнее — Нормально, если вам что-то (или всё) из этого пока непонятно (в том числе, и зачем это использовать) Поговорим о DI, DIP, IoC
  4. — Dependency Inversion Principle и Dependency Injection — разные вещи!

    — DIP — принцип — DI — паттерн — Теперь вы умеете отличать одного от другого Минутка буквоедства!
  5. — That depends — Dishka — Встроенные в fastapi, litestar,

    faststream — Дополнительно: punq, blacksheep + rodi (проплачено Колей Хитровым) DI: финальный шаг — фреймворки
  6. Пример использования Plain FastAPI import svcs async def example_view(rqst): db,

    api, cache =\ await svcs.starlette.aget( rqst, Database, WebAPIClient, Cache ) import svcs @app.get("/") async def example_view( services: svcs.fastapi.DepContainer ): db, api, cache =\ await services.aget( Database, WebAPIClient, Cache )
  7. Как регистрировать >>> import svcs >>> import uuid >>> registry

    = svcs.Registry() >>> registry.register_factory(uuid.UUID, uuid.uuid4) >>> registry.register_value(str, "Hello World") >>> uuid.UUID in registry True >>> str in registry True >>> int in registry False
  8. Практика DIP Что думает автор — Зависеть от деталей плохая

    идея — Делайте зависимости на абстракциях Как в жизни? — Не все умеют «сырой» DI — Мало кто пользуется DI фреймворками — Увидев код с DI, вы (скорее всего) другой писать не захотите — Мысли автора здесь наиболее понятны — Польза от этого принципа наиболее наглядна — DI помогает соблюдать OCP
  9. Вот как выглядит «ручной» DI class AnotherDependency: ... class DependencyForOtherApp:

    ... class OtherApp: ... class ImportantApp: def do_staff(self, other_app: OtherApp): ... ImportantApp().do_staff(OtherApp(DependencyForOtherApp(AnotherDependency())))
  10. — Самый популярный фреймворк имеет почти 4000 звезд — Остальные

    имеют лишь сотни — Всего фреймворков более 15 (не считая built-in) — Я не обозреваю все фреймворки, лишь поверхностно показываю парочку — https://github.com/sfermigier/awesome-dependency-injection-in-python Для начала пара цифр
  11. DI — fastapi from typing import Annotated from fastapi import

    Depends, FastAPI app = FastAPI() 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
  12. — Тесная связь с фреймворком — Нестандартный подход — Один

    общий глобальный стейт — Специфичный код Минусы fastapi di
  13. DI — litestar class MyController(Controller): path = "/controller" dependencies =

    {"controller_dependency": Provide(list_fn)} @get(path="/handler", dependencies={"local_dependency": Provide(int_fn)}) def my_route_handler( self, app_dependency: bool, router_dependency: dict, controller_dependency: list, local_dependency: int, ) -> None: ... my_router = Router( path="/router", dependencies={"router_dependency": Provide(dict_fn)}, route_handlers=[MyController], ) app = Litestar( route_handlers=[my_router], dependencies={"app_dependency": Provide(bool_fn)} )
  14. DI — faststream from faststream import Depends, Logger async def

    base_dep(user_id: int) -> bool: return True @broker.subscriber("in-test") async def base_handler(user: str, logger: Logger, dep: bool = Depends(base_dep)): assert dep is True logger.info(user)
  15. — Написан на Cython — Есть «провайдеры» — Есть «оверрайд»

    — Умеет собирать зависимости в контейнеры — Поддерживает асинхронные «инжекты» — Имеет «wiring» (прослушка) Интересные факты
  16. Провайдер фабрика from dependency_injector import containers, providers class User: ...

    class Container(containers.DeclarativeContainer): user_factory = providers.Factory(User) if __name__ == "__main__": container = Container() user1 = container.user_factory() user2 = container.user_factory()
  17. Сложный провайдер фабрика class Photo: ... class User: def __init__(self,

    uid: int, main_photo: Photo) -> None: self.uid = uid self.main_photo = main_photo class Container(containers.DeclarativeContainer): photo_factory = providers.Factory(Photo) user_factory = providers.Factory( User, main_photo=photo_factory, ) if __name__ == "__main__": container = Container() user1 = container.user_factory(1) # Same as: # user1 = User(1, main_photo=Photo() user2 = container.user_factory(2) # Same as: # user2 = User(2, main_photo=Photo()) another_photo = Photo() user3 = container.user_factory( uid=3, main_photo=another_photo, ) # Same as: # user3 = User(uid=3, main_photo=another_photo)
  18. Сумасшедший провайдер фабрика class Client: ... class Service: def __init__(self)

    -> None: self.client = None class Container(containers.DeclarativeContainer): client = providers.Factory(Client) service = providers.Factory(Service) service.add_attributes(client=client) if __name__ == "__main__": container = Container() service = container.service() assert isinstance(service.client, Client)
  19. Сборка провайдеров provider1() │ ├──> provider2() │ ├──> provider3() │

    │ │ └──> provider4() │ └──> provider5() │ └──> provider6()
  20. Провайдер синглтон from dependency_injector import containers, providers class User: ...

    class Container(containers.DeclarativeContainer): user_factory = providers.Factory(User) if __name__ == "__main__": container = Container() user1 = container.user_factory() user2 = container.user_factory()
  21. Провайдер ресурс def init_resource(argument1=..., argument2=...): resource = SomeResource() # initialization

    yield resource # shutdown ... class Container(containers.DeclarativeContainer): resource = providers.Resource( init_resource, argument1=..., argument2=..., )
  22. — IoC — термин многогранный и с длинной историей —

    Сейчас трактуется как «фреймворк, управляющий созданием объектов и их зависимостей с помощью конфигурации, обычно в декларативном стиле» — В dependency injector ioc контейнер — это класс, в котором лежит декларативное описание зависимостей (+- близкое понятие) IoC контейнер
  23. Контейнеры def init_resource(argument1=..., argument2=...): resource = SomeResource() # initialization yield

    resource # shutdown ... class Container(containers.DeclarativeContainer): resource = providers.Resource( init_resource, argument1=..., argument2=..., )
  24. Wiring from dependency_injector import containers, providers from dependency_injector.wiring import Provide,

    inject class Service: ... class Container(containers.DeclarativeContainer): service = providers.Factory(Service) @inject def main(service: Service = Provide[Container.service]) -> None: ... if __name__ == "__main__": container = Container() container.wire(modules=[__name__]) main() # зависимость подключится и соберется автоматом
  25. — Самый взрослый ✅ — Крутая дока ✅ — Умеет

    всё и немного больше ✅ — Сложный и даже кажется перегруженным — Мы его использовали долго + я в целом к нему неплохо отношусь ✅? — Глобальный скоуп ❌ Тезисно по dependency injector
  26. by

  27. That depends в коде — простой пример import random from

    that_depends import BaseContainer, providers class DIContainer(BaseContainer): random_number = providers.Factory(random.random) numbers_sequence = providers.List(random_number, random_number) DIContainer.numbers_sequence.sync_resolve()
  28. Dishka — скоупы from dishka import provide, Provider, Scope class

    MyProvider(Provider): @provide(scope=Scope.APP) def get_a(self) -> A: return A() @provide(scope=Scope.REQUEST) def get_b(self, a: A) -> B: return B(a) provider = MyProvider()
  29. Dishka — fastapi from dishka.integrations.fastapi import ( FromDishka, inject, setup_dishka,

    ) @router.get("/") @inject async def index(a: FromDishka[A]) -> str: ... ... setup_dishka(container, app)
  30. Dishka — контейнеры from dishka import make_container, Provider, provide, Scope

    class MyProvider(Provider): @provide def get_connection(self) -> Iterable[Connection]: conn = connect(uri) yield conn conn.close() gateway = provide(Gateway) container = make_container(MyProvider(scope=Scope.APP))
  31. — контейнеры/IoC ✅❌ — специфическая документация ✅❌ (автор поясняет) —

    скоупы ✅❌ (нет понимания как это работает) — много интеграций ✅ Тезисно про dishka
  32. — async first ✅ — контейнеры/IoC ✅ — 100% покрытие

    аннотациями + strict mypy ✅ — совместим с fastapi и litestar ✅ — мы используем его в продакшене ✅ — оверрайды для тестов ✅ — нет зависимостей, простая кодовая база, нет cython ✅ — простой ✅ — нет wiring ✅❌ Тезисно про that depends
  33. That depends в коде — простой пример import random from

    that_depends import BaseContainer, providers class DIContainer(BaseContainer): random_number = providers.Factory(random.random) numbers_sequence = providers.List(random_number, random_number) DIContainer.numbers_sequence.sync_resolve()
  34. That depends в коде — IoC from that_depends import BaseContainer,

    providers class DIContainer(BaseContainer): sync_resource = providers.Resource(create_sync_resource) async_resource = providers.Resource(create_async_resource) simple_factory = providers.Factory(SimpleFactory, dep1="text", dep2=123) dependent_factory = providers.Factory( sync_resource=sync_resource, async_resource=async_resource, )
  35. That depends в коде — inject from that_depends import inject,

    Provide @inject def some_function( simple_factory: container.SimpleFactory = Provide[container.DIContainer.simple_factory], default_zero: int = 0, ) -> None: assert simple_factory.dep1 assert default_zero == 0
  36. That depends в коде + fastapi @contextlib.asynccontextmanager async def lifespan_manager(_:

    fastapi.FastAPI) -> typing.AsyncIterator[None]: try: yield finally: await container.DIContainer.tear_down() app = fastapi.FastAPI(lifespan=lifespan_manager) @app.get("/") async def read_root( some_dependency: typing.Annotated[container.DependentFactory, fastapi.Depends(container.DIContainer.dependent_factory)], ) -> datetime.datetime: return some_dependency.async_resource client = TestClient(app) response = client.get("/") assert response.status_code == status.HTTP_200_OK assert response.json() == "async resource"
  37. — Принцип DIP полезен — DI нужен — DI фреймворк

    полезен — Я бы взял либо dependency injector, либо that depends — Dishka неплохой — С fastapi, faststream, litestar и blacksheep DI фреймворки можно сочетать, но в большинстве случаев вы это делать не будете Выводы