Slide 1

Slide 1 text

Нужен ли нам в python DI и какой? Денис Аникин, https://xfenix.ru

Slide 2

Slide 2 text

Что я такое? — Я техлид/комьюнити лид — fullstack, python, typescript, devops, микросервисы, kubernetes — Выступаю на конференциях — Отвечаю за внутреннее сообщество питонистов https://xfenix.ru Кто я?

Slide 3

Slide 3 text

Почему и зачем

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Вронг

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Dependency inversion Принцип инверсии зависимостей DIP из SOLID

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

— Dependency Inversion Principle и Dependency Injection — разные вещи! — DIP — принцип — DI — паттерн — Теперь вы умеете отличать одного от другого Минутка буквоедства!

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

DI Помогает достигать DIP (в моей интерпретации)

Slide 14

Slide 14 text

DI: без него class ImportantApp: def do_staff(self): ... self.other_app = OtherApp() ...

Slide 15

Slide 15 text

DI: первые шаги class ImportantApp: def do_staff(self, other_app: OtherApp): ...

Slide 16

Slide 16 text

DI: типовой класс @dataclass.dataclass(frozen=True, slots=True, kw_only=True) class ImportantApp: other_app: OtherApp def do_staff(self): ...

Slide 17

Slide 17 text

— That depends — Dishka — Встроенные в fastapi, litestar, faststream — Дополнительно: punq, blacksheep + rodi (проплачено Колей Хитровым) DI: финальный шаг — фреймворки

Slide 18

Slide 18 text

Но не только DI есть кое-что ещё

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

Пример использования 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 )

Slide 21

Slide 21 text

Как регистрировать >>> 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

Slide 22

Slide 22 text

Практика DIP Что думает автор — Зависеть от деталей плохая идея — Делайте зависимости на абстракциях Как в жизни? — Не все умеют «сырой» DI — Мало кто пользуется DI фреймворками — Увидев код с DI, вы (скорее всего) другой писать не захотите — Мысли автора здесь наиболее понятны — Польза от этого принципа наиболее наглядна — DI помогает соблюдать OCP

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

Давайте отбросим сложную философию

Slide 25

Slide 25 text

С DI приятнее :) но нужен ли фреймворк для него?

Slide 26

Slide 26 text

Вот как выглядит «ручной» DI class AnotherDependency: ... class DependencyForOtherApp: ... class OtherApp: ... class ImportantApp: def do_staff(self, other_app: OtherApp): ... ImportantApp().do_staff(OtherApp(DependencyForOtherApp(AnotherDependency())))

Slide 27

Slide 27 text

Поэтому…

Slide 28

Slide 28 text

Обсудим фреймворки И заодно глянем код с DI

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

М — масштабы

Slide 31

Slide 31 text

Встроенные DI из коробок

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

— Тесная связь с фреймворком — Нестандартный подход — Один общий глобальный стейт — Специфичный код Минусы fastapi di

Slide 34

Slide 34 text

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)} )

Slide 35

Slide 35 text

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)

Slide 36

Slide 36 text

Dependency injector Самый зрелый фреймворк

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

— Написан на Cython — Есть «провайдеры» — Есть «оверрайд» — Умеет собирать зависимости в контейнеры — Поддерживает асинхронные «инжекты» — Имеет «wiring» (прослушка) Интересные факты

Slide 40

Slide 40 text

Что. Это. Всё? Сейчас покажу откуда готовился DI

Slide 41

Slide 41 text

Провайдер позволяет создавать объекты (наша основная рабочая лошадка, по сути)

Slide 42

Slide 42 text

Провайдер фабрика 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()

Slide 43

Slide 43 text

Сложный провайдер фабрика 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)

Slide 44

Slide 44 text

Сумасшедший провайдер фабрика 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)

Slide 45

Slide 45 text

Сборка провайдеров provider1() │ ├──> provider2() │ ├──> provider3() │ │ │ └──> provider4() │ └──> provider5() │ └──> provider6()

Slide 46

Slide 46 text

Провайдер синглтон 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()

Slide 47

Slide 47 text

Ключевая беда синглтона это мультипоточность (и они про неё знают)

Slide 48

Slide 48 text

Провайдер ресурс def init_resource(argument1=..., argument2=...): resource = SomeResource() # initialization yield resource # shutdown ... class Container(containers.DeclarativeContainer): resource = providers.Resource( init_resource, argument1=..., argument2=..., )

Slide 49

Slide 49 text

Как синглтон только с init и shutdown

Slide 50

Slide 50 text

— IoC — термин многогранный и с длинной историей — Сейчас трактуется как «фреймворк, управляющий созданием объектов и их зависимостей с помощью конфигурации, обычно в декларативном стиле» — В dependency injector ioc контейнер — это класс, в котором лежит декларативное описание зависимостей (+- близкое понятие) IoC контейнер

Slide 51

Slide 51 text

Контейнеры def init_resource(argument1=..., argument2=...): resource = SomeResource() # initialization yield resource # shutdown ... class Container(containers.DeclarativeContainer): resource = providers.Resource( init_resource, argument1=..., argument2=..., )

Slide 52

Slide 52 text

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() # зависимость подключится и соберется автоматом

Slide 53

Slide 53 text

И тд список не полный

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Dishka Свежий фреймворк

Slide 57

Slide 57 text

by

Slide 58

Slide 58 text

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()

Slide 59

Slide 59 text

Киллер feature фреймворка dishka

Slide 60

Slide 60 text

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()

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

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)

Slide 63

Slide 63 text

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))

Slide 64

Slide 64 text

— контейнеры/IoC ✅❌ — специфическая документация ✅❌ (автор поясняет) — скоупы ✅❌ (нет понимания как это работает) — много интеграций ✅ Тезисно про dishka

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

That depends Если очень нравится injector, но не очень заходит cython + wiring

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

— async first ✅ — контейнеры/IoC ✅ — 100% покрытие аннотациями + strict mypy ✅ — совместим с fastapi и litestar ✅ — мы используем его в продакшене ✅ — оверрайды для тестов ✅ — нет зависимостей, простая кодовая база, нет cython ✅ — простой ✅ — нет wiring ✅❌ Тезисно про that depends

Slide 70

Slide 70 text

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()

Slide 71

Slide 71 text

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, )

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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"

Slide 74

Slide 74 text

Итоги Попытаемся их подвести

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

Спасибо! https://xfenix.ru