Как избежать утечек ресурсов и реализовать финализацию зависимостей в библиотеке Dependency Injection?
Что проверяют
Понимаете ли вы, кто владеет ресурсом, созданным контейнером, и в какой момент он обязан его закрыть. Следом обычно просят объяснить, почему __del__ для этого не годится и что будет с соединением, если обработчик упал с исключением.
Как отвечать
Начните с правила: кто создал ресурс, тот его и закрывает. В DI это значит, что контейнер отвечает не только за сборку объекта, но и за его освобождение, а время жизни задаётся областью:
- singleton: живёт всё время работы приложения и закрывается при остановке. Пул соединений с базой, HTTP-клиент;
- scoped (на запрос или задачу): создаётся на входе в область и закрывается на выходе. Сессия ORM, транзакция;
- transient: новый объект на каждое обращение, и тут важно, чтобы у него вообще был владелец.
Механика в Python одна: фабрика оформляется генератором или контекстным менеджером. Всё до yield это создание, всё после, обычно в finally, освобождение. Так устроены зависимости с yield в FastAPI и providers.Resource в dependency-injector, где закрытие запускается через shutdown_resources(). Если пишете контейнер сами, копите открытые ресурсы в contextlib.ExitStack (для асинхронного кода AsyncExitStack): он закроет их в обратном порядке и не бросит остальные, если один из выходов упал с исключением.
Синглтоны закрываются в хуке остановки приложения, в FastAPI это lifespan. Как страховку можно повесить weakref.finalize, но не как основной механизм.
Искать утечки помогают tracemalloc из стандартной библиотеки и режим python -X dev, в котором видны ResourceWarning о незакрытых файлах и сокетах.
Где спотыкаются
Закрывают ресурсы в __del__. Момент вызова не определён, при завершении интерпретатора он может не случиться вовсе, исключение внутри только печатается в stderr, а асинхронное соединение там не закрыть, потому что await недоступен.
Внедряют короткоживущий объект в синглтон. Сессия, захваченная долгоживущим сервисом, переживает свой запрос и держит соединение из пула, и через час пул пуст.
Проверяют только память. Утечка в DI чаще выглядит как исчерпанные соединения или файловые дескрипторы, а объём памяти при этом почти не растёт.