ЗАМЕР
bench/di/graph.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/system-design/dependency-injection/di-containers
- Прогон
- 3.13.7, dishka 1.10.1, dependency-injector 4.49.1
- Как запустить
pip install "dishka==1.10.1" "dependency-injector==4.49.1" python3.13 bench/di/graph.py python3.13 bench/di/cost.py
Запись прогона
Замеры для статьи «DI-контейнер в Python: dishka против dependency-injector»
| Скрипт | Что делает |
|---|---|
graph.py |
наблюдения без времени: одна ли сессия на запрос, когда обнаруживается нехватка зависимости, что будет, если забыть настроить, и что делает несовпадающий маркер |
cost.py |
цена: один «запрос», обращение к общему объекту, второе обращение внутри запроса, создание контейнера |
pip install "dishka==1.10.1" "dependency-injector==4.49.1"
python3.13 bench/di/graph.py
python3.13 bench/di/cost.py
Версии закреплены намеренно: сравниваются не «библиотеки вообще», а две
конкретные сборки, и через полгода числа нужно снимать заново.
dependency_injector.providers — скомпилированный модуль (providers.abi3.so,
Cython), dishka — чистый Python; это проверяется одной строкой
print(providers.__file__) и объясняет половину второго блока.
Что здесь важно прочитать правильно
Первый блок cost.py нельзя читать без graph.py. Его строки делают
РАЗНУЮ работу, и в этом всё дело. dishka входит в scope, собирает граф и на
выходе закрывает сессию. Factory у dependency-injector собирает граф и не
закрывает ничего — закрывать некому, понятия границы запроса у него нет. Он
же выдаёт ДВЕ разные сессии двум репозиториям одного запроса. «Быстрее» здесь
означает «сделал меньше».
Единственное честное сравнение реализаций — второй блок. Обе библиотеки делают там одно и то же: отдают уже созданный объект. Там dependency-injector быстрее в 6 раз, и это прямое следствие компиляции.
Одинаковую работу выполняют dishka_request() и context_request() —
одна сессия на запрос, граница запроса проведена. На них порядок обратный.
Что получилось (3.13.7, dishka 1.10.1, dependency-injector 4.49.1)
Один «запрос» — собрать UnitOfWork из двух репозиториев:
| что | нс | что при этом делается |
|---|---|---|
| руками | 231 | одна сессия, закрывается в finally |
dishka, with container() |
2462 | scope, граф, finalizer закрывает сессию |
| dishka, только вход и выход из scope | 785 | без единого resolve |
dependency-injector, Factory |
1840 | граф; две сессии; не закрывается |
dependency-injector, ContextLocalSingleton + reset() |
5583 | одна сессия, граница вручную |
Остальное:
| что | dishka | dependency-injector |
|---|---|---|
| обращение к общему на приложение объекту | 238 нс | 40 нс |
| второе обращение за тем же объектом в одном запросе | 175 нс | 2162 нс |
| создание контейнера | 969 мкс | 170 мкс |
От запуска к запуску: dishka_request() 2462–2595, factory_request()
1840–2278, context_request() 5583–5875, FACTORY.pool() 40–42,
DISHKA.get(Pool) 224–238.
Наблюдения graph.py
- Одна сессия на запрос. dishka в
Scope.REQUEST— одна на оба репозитория, закрывается на выходе.Factory— две разные, не закрывается ни одна.ContextLocalSingleton— одна, но следующий «запрос» получает ту же самую, пока кто-нибудь не вызоветreset(). - Нехватка зависимости. dishka —
GraphMissingFactoryErrorпри СОЗДАНИИ контейнера. dependency-injector — контейнер создаётся молча, при первом вызове приходитTypeErrorиз__init__вашего класса. Естьcheck_dependencies(), но вызывать её нужно самому, и покрывает она только явныеproviders.Dependency. - Забыли настроить. У dependency-injector не вызванный
wire()не даёт ошибки вовсе: в аргумент приходит сам объект-маркерProvide. У dishka необёрнутый обработчик падает сразу:TypeError: missing 1 required positional argument. - Маркер, не совпадающий с аннотацией.
Provide[Container.config]в параметре, объявленном какService, подставляетConfig. Ни контейнер, ни mypy об этом не сообщают. - Обращение не в том scope. dishka —
NoFactoryErrorс указанием scope. У dependency-injector такой ошибки быть не может: понятия scope в нём нет.
Проверка типов
Отдельно, mypy 2.3.1:
mypy тест.py
container.get(Service)у dishka иcontainer.service()у dependency-injector — оба типизированы верно, mypy ловит присваивание результата в переменную другого типа. Расхожее «dependency-injector не типизирован» на этом примере не подтверждается.- А вот подмену внутри
Provide[...]mypy не видит: параметр, объявленныйServiceи получающийConfig, проходит проверку.
Что этими числами доказать НЕЛЬЗЯ
Что одна библиотека быстрее другой. Быстрее — на разной работе разные: dependency-injector на выдаче готового объекта (в 6 раз), dishka на всём, что связано с границей запроса (в 2,3 раза на одинаковой задаче и в 12 раз на повторном обращении). Выбор между ними определяется не этими числами, а тем, нужна ли вам граница запроса как понятие.
Скрипт
305 строк"""
Наблюдения для статьи «DI-контейнер в Python: dishka против dependency-injector».
ЗАПУСК (снято на 3.13.7, dishka 1.10.1, dependency-injector 4.49.1):
pip install "dishka==1.10.1" "dependency-injector==4.49.1"
python3.13 bench/di/graph.py
ЗДЕСЬ НЕТ ЗАМЕРОВ ВРЕМЕНИ. Скрипт отвечает на вопросы «один это объект или
два» и «на каком этапе сломалось» — то есть на `is` и на имя исключения.
Время меряется отдельно, в `cost.py`.
ПОЧЕМУ ЭТО ВАЖНЕЕ ВРЕМЕНИ. Библиотеки сравнивают по скорости, а расходятся
они в другом: в том, ЧТО именно они делают за запрос и КОГДА сообщают об
ошибке. Пока это не измерено, сравнение времени вводит в заблуждение —
быстрее оказывается тот, кто делает меньше.
ЧЕСТНОСТЬ СРАВНЕНИЯ. Каждая проверка ставится обеим библиотекам, включая те,
где неудобно оказывается той, что понравилась автору. Раздел 5 — специально
симметричный: «что будет, если забыть настроить» спрашивается у обеих.
"""
import sys
from collections.abc import Iterable
def header(title: str) -> None:
print(f"\n--- {title} ---")
# --------------------------------------------------------------------------
# Общий доменный граф. Одинаковый для обеих библиотек — иначе сравнивать
# нечего: разница должна быть в контейнере, а не в примере.
# --------------------------------------------------------------------------
class Config:
pass
class Pool:
def __init__(self, config: Config) -> None:
self.config = config
class Session:
"""Считает сама себя: по номеру видно, одна она на запрос или несколько."""
created = 0
def __init__(self, pool: Pool) -> None:
Session.created += 1
self.number = Session.created
self.closed = False
def close(self) -> None:
self.closed = True
class UserRepo:
def __init__(self, session: Session) -> None:
self.session = session
class OrderRepo:
def __init__(self, session: Session) -> None:
self.session = session
class UnitOfWork:
"""Две репозитория в одном запросе — место, где всё и ломается."""
def __init__(self, users: UserRepo, orders: OrderRepo) -> None:
self.users = users
self.orders = orders
def observe_shared_session() -> None:
header("1. Одна сессия на запрос — или по одной на каждого, кто её попросил")
from dishka import Provider, Scope, make_container, provide
class DishkaProvider(Provider):
config = provide(Config, scope=Scope.APP)
pool = provide(Pool, scope=Scope.APP)
users = provide(UserRepo, scope=Scope.REQUEST)
orders = provide(OrderRepo, scope=Scope.REQUEST)
uow = provide(UnitOfWork, scope=Scope.REQUEST)
@provide(scope=Scope.REQUEST)
def session(self, pool: Pool) -> Iterable[Session]:
session = Session(pool)
yield session
session.close()
container = make_container(DishkaProvider())
with container() as request:
unit = request.get(UnitOfWork)
same = unit.users.session is unit.orders.session
numbers = (unit.users.session.number, unit.orders.session.number)
print(f"dishka, Scope.REQUEST: одна сессия на оба репозитория: {same}, номера {numbers}")
print(f"dishka, закрыта при выходе из scope: {unit.users.session.closed}")
from dependency_injector import containers, providers
class FactoryContainer(containers.DeclarativeContainer):
config = providers.Singleton(Config)
pool = providers.Singleton(Pool, config=config)
session = providers.Factory(Session, pool=pool)
users = providers.Factory(UserRepo, session=session)
orders = providers.Factory(OrderRepo, session=session)
uow = providers.Factory(UnitOfWork, users=users, orders=orders)
unit2 = FactoryContainer().uow()
same2 = unit2.users.session is unit2.orders.session
numbers2 = (unit2.users.session.number, unit2.orders.session.number)
print(f"dependency-injector, Factory: одна сессия на оба репозитория: {same2}, номера {numbers2}")
print(f"dependency-injector, Factory, закрыта: {unit2.users.session.closed} (закрывать некому)")
class ContextContainer(containers.DeclarativeContainer):
config = providers.Singleton(Config)
pool = providers.Singleton(Pool, config=config)
session = providers.ContextLocalSingleton(Session, pool=pool)
users = providers.Factory(UserRepo, session=session)
orders = providers.Factory(OrderRepo, session=session)
uow = providers.Factory(UnitOfWork, users=users, orders=orders)
ctx = ContextContainer()
first = ctx.uow()
print(
"dependency-injector, ContextLocalSingleton: одна сессия на оба репозитория: "
f"{first.users.session is first.orders.session}, номера "
f"{(first.users.session.number, first.orders.session.number)}"
)
second = ctx.uow()
print(f" следующий «запрос» БЕЗ reset(): та же сессия: {second.users.session is first.users.session}")
ctx.session.reset()
third = ctx.uow()
print(f" после ctx.session.reset(): новая сессия: {third.users.session is not first.users.session}")
print()
print("Читать так: у dishka границу запроса рисует контекстный менеджер, и")
print("finalizer закрывает сессию на выходе. У dependency-injector Factory")
print("выдаёт НОВУЮ сессию каждому, кто её попросил, а ContextLocalSingleton")
print("выдаёт одну — но границу «запроса» проводит reset(), который вызываете")
print("вы сами и в каждой ветке кода.")
def observe_error_timing() -> None:
header("2. На каком этапе становится известно, что зависимости не хватает")
class Missing:
pass
class Service:
def __init__(self, config: Config, missing: Missing) -> None:
pass
from dishka import Provider, Scope, make_container, provide
class Incomplete(Provider):
config = provide(Config, scope=Scope.APP)
service = provide(Service, scope=Scope.REQUEST)
stage = "создание контейнера"
try:
container = make_container(Incomplete())
stage = "первый resolve"
with container() as request:
request.get(Service)
stage = "не упало вовсе"
except Exception as exc:
print(f"dishka: {type(exc).__name__} на этапе «{stage}»")
from dependency_injector import containers, providers
class Forgot(containers.DeclarativeContainer):
config = providers.Singleton(Config)
service = providers.Factory(Service, config=config) # missing не передан
forgot = Forgot()
print("dependency-injector: контейнер создан без ошибки")
try:
forgot.service()
except Exception as exc:
print(f"dependency-injector: {type(exc).__name__} при первом вызове: {exc}")
class Declared(containers.DeclarativeContainer):
missing = providers.Dependency(instance_of=Missing)
config = providers.Singleton(Config)
service = providers.Factory(Service, config=config, missing=missing)
declared = Declared()
try:
declared.check_dependencies()
except Exception as exc:
print(f"dependency-injector: check_dependencies() находит это явно: {exc}")
print(" но вызвать её нужно самому, и покрывает она только providers.Dependency,")
print(" а не забытый аргумент конструктора из предыдущего примера.")
def observe_forgotten_setup() -> None:
header("3. Симметричная проверка: что будет, если забыть настроить")
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject
class Service:
def __init__(self, config: Config) -> None:
self.config = config
class Wired(containers.DeclarativeContainer):
config = providers.Singleton(Config)
service = providers.Singleton(Service, config=config)
@inject
def handler(service: Service = Provide[Wired.service]) -> str:
return type(service).__name__
print(f"dependency-injector, контейнер НЕ подключён (wire не вызван):")
print(f" handler() вернул объект типа {handler()!r} — исключения нет вовсе")
Wired().wire(modules=[sys.modules[__name__]])
print(f" после wire(): {handler()!r}")
from dishka import FromDishka
from dishka.integrations.base import is_dishka_injected
def dishka_handler(service: FromDishka[Service]) -> str:
return type(service).__name__
print("dishka, обработчик НЕ обёрнут:")
print(f" помечен как инжектируемый: {is_dishka_injected(dishka_handler)}")
try:
dishka_handler() # type: ignore[call-arg]
except TypeError as exc:
print(f" вызов: {type(exc).__name__} — {exc}")
def observe_wrong_marker() -> None:
header("4. Маркер, не совпадающий с аннотацией")
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject
class Service:
def __init__(self, config: Config) -> None:
self.config = config
class Mixed(containers.DeclarativeContainer):
config = providers.Singleton(Config)
service = providers.Singleton(Service, config=config)
@inject
def handler(service: Service = Provide[Mixed.config]) -> str:
"""Аннотация обещает Service, маркер указывает на config."""
return type(service).__name__
Mixed().wire(modules=[sys.modules[__name__]])
print(f"объявлено Service, маркер указывает на config — пришёл: {handler()}")
print()
print("Ни контейнер, ни mypy об этом не сообщают: маркер и аннотация не")
print("сверяются между собой. Проверка типов из README этой статьи:")
print(" mypy находит несовпадение у ВОЗВРАЩАЕМОГО значения обеих библиотек,")
print(" но подмену внутри Provide[...] не находит.")
def observe_scope_error() -> None:
header("5. Симметричная проверка: обращение не в том scope")
from dishka import Provider, Scope, make_container, provide
class Service:
def __init__(self, config: Config) -> None:
self.config = config
class P(Provider):
config = provide(Config, scope=Scope.APP)
service = provide(Service, scope=Scope.REQUEST)
container = make_container(P())
try:
container.get(Service)
except Exception as exc:
print(f"dishka, запрос REQUEST-зависимости на APP-уровне: {type(exc).__name__}")
print(f" {str(exc).splitlines()[0]}")
print()
print("У dependency-injector такой ошибки быть не может: понятия scope в нём")
print("нет вовсе, и обратиться «не в том scope» не к чему. Это не достоинство")
print("и не недостаток — это и есть разница в модели.")
def main() -> None:
import importlib.metadata as metadata
print(f"Python {sys.version.split()[0]}")
print(f"dishka {metadata.version('dishka')}, dependency-injector {metadata.version('dependency-injector')}")
observe_shared_session()
observe_error_timing()
observe_forgotten_setup()
observe_wrong_marker()
observe_scope_error()
if __name__ == "__main__":
main()