Рубрики
Без рубрики

DI: инъекция питонической зависимости

В этой статье я хотел бы представить DI, структуру впрыска в зависимости, которая стремится … Tagged с помощью Python, программирования, тестирования, OpenSource.

В этой статье я хотел бы представить di , структура впрыска зависимостей, которая стремится быть:

  • Интуитивно понятный : Простые вещи легко, сложные вещи возможны
  • Лаконичный : Низкий шаблон, может запустить ваш код непосредственно через автоматическое
  • Правильно : Протестировано с Mypy ; Экспортирует действительные аннотации типа
  • Мощный : С жизненными веществами, общими зависимостями и гибкий API
  • Исполнитель : Поддержка асинхронных зависимостей и одновременного исполнения

Введение в инъекцию зависимостей

Инъекция зависимости – это метод для организации более крупных приложений путем передачи контроля ваших зависимостей на «что -то» (часто структура), которая собирает их для вас. Это сложная тема, так что, возможно, лучше просто посмотреть на пример. Мы создадим наш пример вокруг довольно распространенного сценария: у вас есть класс, который должен выполнять веб -запросы, поэтому ему нужен клиент HTTP (мы будем использовать HTTPX).

from httpx import Client


class WeatherClient:
    def __init__(self) -> None:
        self.client = Client()
    def get_temp(self, lat: float, long: float) -> float:
        self.client.get(...)
        ...

Этот класс делает не Используйте инъекцию зависимости. Проблема возникает, когда мы хотим проверить этот класс: мы, вероятно, не хотим делать реальные веб -запросы в нашем модульном тесте.

К счастью, HTTPX предлагает возможность использовать Мик транспорт :

import httpx

def handler(request: httpx.Request) -> httpx.Response:
    return httpx.Response(200, json={"temp": 43.21})

client = httpx.Client(transport=httpx.MockTransport(handler))

Но у нас есть проблема: как мы получаем наш Клиент Использование фиктивного транспорта в нашем WeatherClient ? Один из них часто используется Обезьяна исправления Анкет В то время как Monkey Patching имеет свои варианты использования, это также может привести к множеству очень запутанных проблем и грязного кода, особенно в более крупном проекте.

Инъекция зависимости дает нам альтернативу: WeatherClient принять экземпляр из httpx. Клиент :

from typing import Optional

from httpx import Client


class WeatherClient:
    def __init__(self, client: Optional[Client] = None) -> None:
        self.client = client or Client()

Эта форма инъекции зависимостей называется Инъекция конструктора Потому что мы внедряем зависимость в конструкторе/ __init__ метод

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

Примечание : Вы заметили, что httpx. Клиент Сам использует инъекцию зависимостей для Транспорт параметр ? Окрашенный, да!

Проблемы

Как и выше, инъекция зависимостей не так сумасшедшая, как кажется, это может быть просто. И вы, вероятно, уже используете его в библиотеках, таких как httpx Анкет

Но каковы недостатки?

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

По сути, код в собрать И отслеживать все эти зависимости может стать головной болью сама по себе.

Введите: рамки впрыска зависимостей.

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

Организации впрыска зависимостей

Хотя рамки впрыска зависимостей бывают разных форм и размеров, в принципе все они обеспечивают аналогичную функциональность:

  • Зарегистрируйте зависимости и поставщики этих зависимостей (то есть сообщите структуре, как построить свои зависимости)
  • Введите свои зависимости для вас

Некоторые фреймворки также имеют более продвинутые функции, такие как:

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

di является одной из последних рамок, предлагающих все эти функции и многое другое.

Используя ди

Начнем с простого примера. Мы хотим создать веб -приложение, которое использует наше WeatherClient Анкет

import os
from dataclasses import dataclass, field

from xyz import App


def url_from_env() -> str:
    return os.environ["WEATHER_URL"]


@dataclass
class AppConfig:
     weather_url: str = field(default_factory=url_from_env)


class MyApp:
    def __init__(self, weather_client: WeatherClient) -> None:
    self.weather_client = weather_client

Создание этого вручную может выглядеть примерно как:

app = App(
    weather_client=WeatherClient(
        client=httpx.Client(
            ...
        )
    ),
    config=AppConfig(
        ...
    )
)

Используя di :

from di import Container, Dependant


container = Container()
app = container.execute_sync(
    container.solve(Dependant(App))
)

Обратите внимание, что не имеет значения, насколько глубоки зависимости или сколько их, мы только когда -либо говорили: «Мы хотим экземпляр приложения».

Как насчет инъекции Клиент ?

client = Client(
    transport=MockTransport(handler=handler)
)
container.bind(Dependant(lambda: client), Client)
app = container.execute_sync(
    container.solve(Dependant(App))
)

Опять же, важна здесь, это Клиент Может быть вложенные 10 слоев зависимостей глубоко, и приведенный выше код вообще не изменится.

Расширенная функциональность

Помимо основного использования выше, di Поддерживать намного больше функциональности.

Вот некоторые фрагменты, демонстрирующие функциональность, но для полных примеров и дополнительных объяснений см. В нашем Документы Анкет

Распределение зависимости

from di import Container, Depends, Dependant

def func() -> object:
    return object()

def dependant(
    one: object = Depends(func),
    two: object = Depends(func),
    three: object = Depends(func, share=False)
) -> None:
    assert one is two and two is not three

container = Container()
container.execute_sync(container.solve(Dependant(dependant)))

В этом примере Func называется дважды (по умолчанию в двух отдельных потоках), один раз для один и два , которые имеют одинаковое значение, и один раз для Три Анкет

Прицелы и жизненные циклы

from typing import Generator

from di import Container, Depends, Dependant

def func() -> Generator[int, None, None]:
   print("func startup")
   yield 1
   print("func shutdown")


def dependant(v: int = Depends(func, scope=1234)) -> int:
    print("computing")
    return v + 1

container = Container()
with container.enter_local_scope(1234):
    print("enter scope")
    res = container.execute_sync(
       container.solve(Dependant(dependant))
    )
    print("exit scope")

assert res == 2

Этот пример будет печатать:

enter scope
func startup
computing
func shutdown
exit scope

И эквивалентно:

from contextlib import contextmanager


with contextmanager(func)() as value:
    dependant(v=value)

Прицелы в di являются произвольными: любая хешейная стоимость подойдет. Пока вы согласны в объявлении областей, di не будет заботиться о том, что вы используете. Если «вы» – это структура, вызывая код пользователя, это дает вам свободу определять и управлять своими собственными областями. Для веб -структуры это может быть «приложение» и «запрос». Для CLI может быть единственный «Call». Для TUI может быть «приложение» и «Объем событий»

Решенные зависимости

Часто вы должны запускать одну и ту же зависимость несколько раз, но не динамически не изменяют график зависимости каждый выполнение (вы бы динамически изменили график, если бы вы делали контейнер. Связь между каждым вызовом).

В этих сценариях di Позволяет сохранить весь вычисленный график зависимости в Сохраняющая защита и выполнить один и тот же график несколько раз. На самом деле, это то, что мы уже делали уже каждый раз, когда называли Контейнер.solve Анкет

# solving locks in binds, but not cached values or scopes
solved = container.solve(Dependant(dependency))
res1 = container.execute_sync(solved)
res2 = container.execute_sync(solved)

Асинхронное исполнение

Просто замените execute_sync для execute_async Для асинхронной поддержки:

from di import Container, Dependant


async def dep() -> int:
    return 1

async def main() -> None:
    container = Container()
    solved = container.solve(Dependant(dep))
    res = await container.execute_async(solved)
    assert res == 1

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

В зависимости от индивидуальной протокола

Disecidantprotocol Интерфейс обеспечивает абстрактное представление спецификации зависимости. Контейнер использует это абстрактное представление для построения графиков зависимости.

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

Например, чтобы принять значение по умолчанию вместо Callable:

from di import Dependant
from di.types.providers import DependencyType
from di.types.scopes import Scope

class DefaultDependant(Dependant[DependencyType]):
    def __init__(
        self,
        default: DependencyType,
        scope: Scope,
        shared: bool,
    ) -> None:
        super().__init__(
            call=lambda: default,
            scope=scope,
            share=share,
         )

Теперь, если не будет привязки/переопределения зависимости, она будет выполняться так же, как если бы она была вызвана напрямую.

Исполнители

Один из принципов дизайна di это изолировать работу контейнера на различные шаги:

  • Проводка, которую можно контролировать через зависимость
  • Решение, которое принадлежит контейнеру
  • Выполнение, которое управляется через API исполнителя

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

API исполнителя выглядит следующим образом:

import typing

ResultType = typing.TypeVar("ResultType")

Task = typing.Union[
    typing.Callable[[], None],
    typing.Callable[[], typing.Awaitable[None]]
]


class SyncExecutor(typing.Protocol):
    def execute_sync(
        self,
        tasks: typing.List[typing.List[Task]],
        get_result: typing.Callable[[], ResultType],
    ) -> ResultType:
        ...


class AsyncExecutor(typing.Protocol):
    async def execute_async(
        self,
        tasks: typing.List[typing.List[Task]],
        get_result: typing.Callable[[], ResultType],
    ) -> ResultType:
        ...

Исполнитель по умолчанию ( di.executors. Defaultexecutor ) поддерживает как синхронизацию, так и асинхронное выполнение. Зависимости выполняются одновременно (через потоки в выполнении синхронизации и задачи в асинхронном выполнении) для зависимостей, которые могут быть выполнены одновременно.

Простая реализация Syncexecutor может выглядеть как:

class SimpleExecutor(SyncExecutor):
    def execute_sync(
        self,
        tasks: typing.List[typing.List[Task]],
        get_result: typing.Callable[[], ResultType],
    ) -> ResultType:
        for group in tasks:
            for task in group:
                res = task()
                if inspect.isawaitable(res):
                    raise TypeError
         return get_result()

Обратите внимание, что Syncexecutor может быть вручено асинхронные функции в качестве задач. Синхронизированная/асинхронная часть относится к API самого исполнителя, а не к задачам, которую он может быть вручен. Реализация зависит от того, чтобы как -то поднять исключение или выполнить асинхронную задачу.

Интеграция в другие библиотеки

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

С этой целью мы предоставляем Пример интеграции , в настоящее время только для Starlette и Textual, но приветствуются больше идей!

Производительность

Мы предоставляем несколько Основные тесты по сравнению с FASTAPI Анкет

Результаты сильно зависят от размера DAG.

Для относительно большого, но не совершенно абсурдного Дага из 12 вершин с каждой вершиной, имеющей 0-3 края, di Выработает немного быстрее, чем Fastapi (пара процентов за запрос).

Для больших даг, особенно где di способен выполнять зависимости одновременно, di может быть в 10 раз быстрее, чем FASTAPI. Чтобы быть справедливым, это не реальный сценарий, и Fastapi делает намного больше, чем Starlette di Интеграция делает (отслеживание вещей для OpenAPI и т. Д.). Если эти тесты несправедливы по другим причинам, пожалуйста, дайте мне знать.

Вывод

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

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

Оригинал: “https://dev.to/adriangb/di-pythonic-dependency-injection-5b27”