С
Советы разработчикам (python и не только)
31.05.2026 18:13 · 👁 5K
Юзкейсы, сценарии и интеракторы
Когда мы пишем наш код (программу, сервис, библиотеку), мы так или иначе анализируем зачем она нужна. У пользователей и заказчика продукта есть цели. Бизнес хочет получить прибыль, покупатель - получить товар, пользователь техподдержки - не показаться начальнику глупым, чтобы его не уволили. Есть цели более глобальные, такие как достижение благополучия, есть более конкретные подцели, как доставка товара на конкретный адрес.
Зачастую с системой взаимодействуют не только люди, поэтому часто используют более общий термин — актор.
Анализируя цели и потребности всех участников взаимодействия, мы приходим к формулированию требований, одной из форм которых являются use cases, которые и описывают взаимодействие некоторого актора с системой ради удовлетворения его целей. Каждый юзкейс - это текстовый документ, но они могут дополняться диаграммами. Юзкейсы могут включаться друг в друга или расширять. Например, "ввод адреса" может быть конкретным юзкейсом, использующимся как в юзкейсе "покупка товара", так и в юзкейсе "заказ уборки".
Юзкейс представляет собой некий набор шагов, при этом их можно разделить на основной сценарий и расширения. Cценарий - это последовательность шагов взаимодействия, в то время как юзкейс - набор возможных вариантов развития событий. Шаг в данном случае - ещё не конкретный api вызов, а просто логически выделенная операция, значимое намерение актора или реакция системы. Иногда оно может ссылаться и на другой юзкейс.
Переходя от этапа анализа к разработке, разбираясь с возможными юзкейсами и детализируя взаимодействие, мы уже можем зафиксировать конкретные детали реализации интерфейсов. Таким образом, одно логическое действие превращается в api вызовы, клики мышью и т.п. И в коде мы уже работаем с этими вызовами, хотя мы все ещё пишем программу, реализующую свою часть юзкейсов.
Обработка таких вызовов в коде обычно начинается с контроллера (адаптером между основной частью программы и фреймворком), а затем в реализацию прикладной части бизнес логики (часто используются термины "прикладной сервис" и "интерактор юзкейса"), а далее, возможно, в некие сущности и более универсальный части кода. Количество таких слоев зависит от конкретной методологии и сложности решаемых задач.
Хочу обратить внимание, что несмотря на то, что мы используем термин "интерактор юзкейса" - это лишь часть системы, реагирующая на действия актора. Сами юзкейсы при этом - более теоретическая часть, описывающая требования к нашему продукту как к черному ящику, но мы используем её для проектирования и организации наших интеракторов.
Пример юзкейсов.
Покупка товара
1. Пользователь выбирает товар из каталога
2. Пользователь указывает адрес
3. Пользователь оплачивает товар
Указание адреса
Основной сценарий
1. Пользователь вводит адрес.
2. Система показывает найденные варианты доставки.
3. Пользователь подтверждает выбранный вариант.
Расширение
3.а Пользователя не устраивают предложенные варианты
3.a.1 Пользователь повторяет ввод пока не будет найден подходящий вариант
При этом, если мы разрабатываем веб приложение, то могут быть такие api-вызовы как "GET /address-suggestions/" или "PUT /order/delivery". Для другого типа приложения, детали реализации взаимодействия так же будут отличаться
Дополнительные материалы:
• https://martinfowler.com/bliki/UseCasesAndStories.html
• https://alistaircockburn.com/Articles/Use-Case-Foundation-Ivar-Alistair
• https://habr.com/ru/articles/248063/
• https://blog.cleancoder.com/uncle-bob/2011/11/22/Clean-Architecture.html
С
Советы разработчикам (python и не только)
17.02.2026 09:25 · 👁 11K
Меня позвали поговорить о разном: о проектах, опыте, программировании в целом.
https://www.youtube.com/watch?v=B4XrghSsUhQ
С
Советы разработчикам (python и не только)
11.01.2026 11:54 · 👁 13.2K
SQLAlchemy и ORM
SQLAlchemy в python предоставляет два набора API:
1. Возможность конструировать и выполнять запросы в БД
2. Получать и сохранять сущности
Первый набор api (Core функциональность) - это в первую очередь объекты запросов select, insert, update и других, а также метод execute, который есть у объекта Сonnection (можно использовать Session, но о нем ниже). Часто при этом используются объекты Table для описания структуры БД.
Кроме этого, sqlalchemy можно использовать как ORM. Это означает, что вы создаете классы, описывающие ваши сущности, а затем все операции с БД делаете только через них: вы загружаете сущности, сохраняете сущности, удаляете сущности (см. пост про стейт приложения и БД). При этом Connection вам уже недостаточно, вам требуется Session.
В чем ключевые отличия ORM подхода от Core?
• Для загрузки сущностей используется session.get для получения по первичному ключу или select(Model) запрос в других случаях. Обратите внимание, что речь идет именно о загрузке моделей целиком, а не отдельных полей таблиц. Кроме того, сессия реализует паттерн IdentityMap, то есть если объект уже загружен в память, то в некоторых случаях повторный запрос в БД не требуется (при get или загрузке связанных моделей), а если БД вернет одну сущность несколько раз, будет переиспользован один экземпляр.
• Для добавления новых сущностей мы используем метод session.add. Сессия реализует паттерн Unit of Work, поэтому все добавленные сущности будут хранится в промежуточном буфере и сохранятся при flush или commit. Если сущности содержат связи, они тоже будет автоматически добавлены. При чём алхимия следит за правильным порядком вставки и даже пытается оптимизировать запросы.
• Для применения изменений надо просто поменять сущности и закоммитить транзакцию. Опять же, благодаря Unit of work все изменения отслеживаются автоматически и не требуется вызывать никаких методов сохранения на каждом экземпляре вручную.
• Для удаления есть отдельный метод UoW - session.delete. Кроме непосредственно удаления сущности он так же следит за связями и обрабатывает их, зачастую более сложным способом чем умеет сама СУБД.
Таким образом, если вы используете модели алхимии, но вызываете insert или update, вы фактически не используете ORM. Так можно делать в целях оптимизации в отдельных нагруженных частях программы, но оно не должно восприниматься как основной способ работы с ORM.
Другой ошибкой является создание отдельных доменных сущностей одновременно с моделями ORM. Из-за этого приходится писать достаточно сложные мапперы, которые часто не учитывают наличие IdM и UoW, что может привести к некорректной логике сохранения данных. Вместо этого можно выделить три подхода:
• Не использовать ORM. Пишите сущности как вам удобно и сохраняйте в БД используя core-функциональность. Вы лишаетесь встроенного UoW, но и не конфликтуете с ним.
• Использовать модели ORM как бизнес сущности. Ваши сущности выглядят чуть более грязно, однако это только метаданные, которые в БЛ использоваться не будут.
• Использовать датаклассы и императивный маппинг, описывая отдельно Table объекты. Вы все ещё ограничены тем, что алхимия меняет модели, внедряя скрытые атрибуты, но ваша БЛ уже это не видит. Вы не можете использовать совсем кастомные мапперы для отдельных моделей, но зато имеете все возможности алхимии.
Даже используя ORM приходится писать сложные select запросы для реализации поиска или получения сводной информации, поэтому имеет смысл реализовать паттерн репозиторий, скрывая их построение внутри, но при этом мы все ещё полагаемся на возможности сессии как UoW.
Кроме того, в нашем приложении кроме логики работы с сущностями могут существовать запросы чтения, возвращающие клиенту определенный срез данных. Для этого мы можем реализовать простой DAO, использующий Core алхимии или описать альтернативные модели, которые будут императивно маппиться на те же таблицы, но иметь другую структуру.
Дополнительные материалы:
• Mapping SQLAlchemy to dataclass
• Mapping a Class against Multiple Tables
• Patterns Implemented by SQLAlchemy
С
Советы разработчикам (python и не только)
04.11.2025 12:30 · 👁 13.7K
Asyncio и колбэки
В прошлом посте мы рассмотрели как asyncio взаимодействует с генераторами, но на более низком уровне он взаимодействует с колбэками, а генераторы и корутины - вспомогательные возможности для более удобного написания кода.
Базовая функциональность лупа заключается в том, что он умеет вызывать в цикле синхронные функции. Пока один колбэк не отработает, луп не перейдет к другому. Такие функции можно добавить через вызов loop.call_soon (или loop.call_soon_threadsafe, если мы хотим это сделать из другого потока). Дополнительно, у него есть некоторые другие функции вроде задания задержки вызова функции.
Следующий важный объект - Future. Его задача достаточно простая - хранить некоторый результат и вызывать колбэк при его установке. И все ещё это обычная синхронная функция, но уже вызывающаяся не планировщиком лупа, а напрямую, когда кто-то делает set_result
Однако взаимодействие с лупом через колбэки многим кажется достаточно неудобным, в связи с чем и было предложено поддержать генераторы, которые в дальнейшем превратились в async функции.
В качестве клея между асинк функциями (читай генераторами) и логикой вызова функций лупом выступает объект Task. Таск добавляет свой метод как колбэк в лупе и когда луп вызовет его, он промотает внутренний генератор на шаг вперед. Если при этом встретится Future, таск зарегистрируется в нем на ожидание результата и, таким образом, шаг будет завершен и управление вернется в луп. Когда кто-то установит результат на футуре, он сразу обратится к нашему таску и сможет передать ему управление.
При этом возникает важный вопрос, а кто пометит футуру завершенной? У нас есть несколько вариантов:
• Другой таск или колбэк, просто вызвав set_result
• Другой поток (так как объекты asyncio не потокобезопасны, это будет сделано через call_soon_threadsafe). Например, так работает aiofiles или granian.
• Сам луп согласно своей внутренней логике
Asyncio loop имеет множество методов, которые возвращают Future объекты и при этом включают какую-то внутреннюю логику. Одна из важнейших функций - работа с сокетами. Операционная система имеет два режима работы с сокетами - блокирующий и неблокирующий. В блокирующем режиме наши вызовы send/recv блокируют тред пока сокет не будет готов, в неблокирующем же - мы сразу получим предложение повторить вызов позже. Благодаря этому можно во время ожидания переключиться на другую работу. Дополнительно, ОС имеют разные варианты API для опроса сокетов на предмет доступности (вы, конечно, можете пытаться делать send/recv на каждом в цикле, но это очень не эффективно): select, epoll, kqueue, IOCP.
Таким образом, loop имеет методы вида sock_recv, sock_sendall которые возвращают Future и говорят лупу заниматься обслуживанием таких сокетов (опрашивать готовность и собственно передать данные). Дальнейшее поведение лупа будет зависеть от реализации, но в случае Linux это будет вызов epoll между вызовами колбэков. Как только чтение/запись в сокет будет завершено, луп сам (или какая-то его подсистема, например Транспорт) установит результат футуре. Аналогично можно работать с pipe и сабпроцессами.
Кратко:
1. Loop умеет вызывать синхронные колбэки
2. Future хранит результат и вызывает колбэки
3. Task адаптирует генератор под колбэки лупа и связывает с футурами
4. Loop сам опрашивает сокеты в неблокирующем режиме и обновляет футуры
5. Футуры можно обновлять из других колбэков или тредов
Дополнительные материалы:
• https://peps.python.org/pep-3156
• https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports
• https://www.kegel.com/c10k.html
• https://man7.org/linux/man-pages/man7/io_uring.7.html
С
Советы разработчикам (python и не только)
27.09.2025 17:51 · 👁 13.7K
Стейт при работе с базами данных
Подходы работы с БД можно разделить на две группы:
• Ориентированные на состояние в памяти (ActiveRecord, Data Mapper, UoW)
• Ориентированные на действия с состоянием в БД (DAO)
Если ты ориентирован на состояние в памяти, то фактически твоё приложение имеет вид
1. Загрузил состояние сущностей
2. Выполнил необходимые операции по его изменению
3. Сохранил состояние
Такой подход позволяет тестировать логику изменения без базы данных, а так же унифицировать взаимодействие с БД, тем самым снижая сложность кода. Однако появляются и ограничения:
• Если кроме указанных операций что-то изменить напрямую в БД, то загруженный стейт перестанет соответствовать состоянию в БД. Любые операции с ним могут привести к некорректному результату. Чтобы этого не произошло, не стоит выполнять insert/update/delete запросы кроме как при сохранении состояния после изменения.
• Если одна и та же сущность доступны несколькими путями (например при отношениях многие-ко-многим, но не только), то при загрузке мы можем получить несколько её экземпляров. Изменение одного из экземпляров не будет автоматически отражено во втором и мы получим состояние противоречащее само себе. Чтобы такого избежать, рекомендуется использовать паттерн Identity Map.
• В серверном приложении несколько конкурентных запросов могут пытаться изменить одни сущности, что может привести к порче данных при сохранении. Чтобы этого избежать, можно использовать разные виды блокировок, а также следить чтобы всегда грузился полный набор связанных объектов, нужных для контроля правил.
В подходе, ориентированном на прямое изменение состояния в БД мы храним в памяти минимальный набор данных, необходимый для выполнения запросов. При правильной организации кода мы иногда можем использовать БД более эффективно за счет уменьшения количества передаваемых данных. Однако,
• Часть бизнес логики переезжает в запросы или в хранимые процедуры.
• Блокировки все ещё могут быть нужны, но часто они могут быть выставлены СУБД автоматически.
• API хранилища усложняется, появляется много очень разнообразных методов, теряется унификация
Первый подход становится наиболее актуален при наличии сложной модели данных, когда есть большое количество разных сущностей или при работе с ними требуется сложная бизнес логика. Второй подход полезен когда важнее оптимизация (например при действительно высокой нагрузке) или когда используются специфические хранилища, не позволяющие реализовать полноценное сохранение сущностей.
При этом, даже ориентируясь на работу с состоянием, может быть актуально для эффективности отдельные операции вынести в БД (в основном массовые), однако при этом нужно быть очень аккуратным.
Так же стоит отметить что паттерн DAO может быть применен для загрузки данных для отображения, когда нам не требуется выполнение бизнес логики, но нужен специфический срез разных частей БД.
Дополнительные материалы:
• https://martinfowler.com/bliki/DDD_Aggregate.html
• https://martinfowler.com/eaaCatalog/identityMap.html
С
Советы разработчикам (python и не только)
07.08.2025 14:01 · 👁 15.6K
Asyncio и цикл событий
Чтобы понять, как рабоатет asyncio, давайте рассмотрим, как в Python работают функции.
Мы можем вызвать функцию, она выполнится. Просто интерпретатор берет её код и выполняет целиком. Если же мы берём функцию-генератор и просто вызываем, ничего не будет происходить. Тебе надо по ней пробежаться циклом или самому вызывая next. Когда мы вызываем next, код функции доходит до следующего yield и останавливается.
Представим, что у нас есть основной части программы цикл, который крутит этот генератор.
def foo():
yield 1
yield 2
for _ in foo():
pass
Если между yield вызывается обычная функция, то она выполняется как обычно, и не важно, что это происходит внутри генератора. А что, если наш генератор внутри себя вызывает другой генератор через yield from?
def foo():
yield 1
yield 2
def bar():
yield from foo()
for _ in bar():
pass
В этом случае, наш основной цикл просто крутится дальше как раньше, но уже выполняется не код первой функции, а переходит к той, что она вызвала и останавливается уже на её yield. То есть мы как бы крутим уже вложенный генератор, а не оригинальный. Нашему циклу не важно, откуда берутся шаги внутри функции: то ли она сама их выдумала, то ли получила из вложенных вызовов.
Проводя аналогию с asyncio наш главный цикл - это event loop, а генераторы - корутины. Корутины могут друг друга вызвать и циклу норм, он продолжает их крутить. Разница чисто синтаксическая: в asyncio используется await вместо yield.
Для полнцоенного использования тут не хватает одной важной детали.
На текущий момент у нас есть цикл, который постоянно дёргает генератор и при завершении шага переходит сразу к следующему. В реальности же мы хотим приостанавливать выполнение до получения данных из сети, наступления определенного времени и т.п. В норме мы хотим отдать управление циклу событий, чтобы он там что-то ещё мог поделать своё пока мы ждем. Но, как только слип завершится или сокет будет готов, надо вернуться назад и продолжить выполнение.
Итого, мы крутим цикл. Там генераторы, вызывающие генераторы. Мы это все крутим, крутим. Получаем оттуда объект, который наш loop умеет ожидать (Future). И каждый раз, прежде чем крутить дальше, ждём что он будет готов.
А раз нас есть цикл, который умеет ждать между вызовами next, он может в это время дёрнуть другой генератор, если у него есть их список. То есть, такие независимые генераторы - это таски asyncio.
Кратко:
1. Когда ты эвейтишь специальный объект, ты делаешь yield и отдаешь управление циклу событий
2. Когда ты эвейтишь другую корутину, ты просто проваливаешся внутрь
3. Когда цикл событий получает управление, он может заняться другими делами
4. У цикла событий дел два: дергать разные таски и ждать если все просили подождать внешнего мира
Дополнительные материалы:
• https://peps.python.org/pep-3156
• https://peps.python.org/pep-0380
• https://t.me/opensource_findings/869
• https://www.gevent.org/intro.html
С
Советы разработчикам (python и не только)
02.04.2025 15:27 · 👁 19.8K
Anti corruption layer
Часто в наших приложениях мы обращаемся к каким-то внешним системам. Иногда мы просто обращаемся к ним, иногда ждем обратных вызовов. Это может быть платежная система, сервис уведомлений, инвентаризации или что-то ещё.
Ключевое в таких интеграциях то, что такие системы имеют свой жизненный цикл, свой контекст и часто их устройство слабо похоже на то, что нам надо от них, однако интеграция с ними может быть жизненно необходима для нашего приложения.
Для того, чтобы сделать наше приложение более устойчивым к изменениям внешнего API и одновременно сохранить нашу логику чистой, мы выделяем отдельный компонент - Anti Corruption Layer. Его задача - скрыть за собой детали взаимодействия с этой системой, предоставив нашей бизнес логике понятное ей API.
Структура с использованием ACL может состоять из таких частей:
• Какая-то часть нашей бизнес-логики использует интеграцию. Она знает о том, что принципиально это за сервис, однако не знает о деталях общения с ним.
• У бизнес-логики есть требования к взаимодействию. Они выражаются в виде интерфейсов и каких-то моделей данных. Хоть мы и абстрагируемся от деталей, мы всё-таки выражаем наше представление о конкретном сервисе, но лишь об интересных нам аспектах и о данных в терминах понятных нашему бизнесу.
• Сторонний сервис для нас представляет некоторое API, удобное или не очень. Иногда это вызовы библиотек, иногда - удаленные обращения по HTTP или другим протоколам. Мы не контролируем код, который это реализует. Это чужой код.
• Если сторонний сервис не предоставляет для нас достаточно понятного API, мы самостоятельно реализуем клиентский фасад. Он обязательно выражается в терминах чужой системы, что позволит нам при её изменениях проще его обновлять. Он может реализовывать только часть вызовов, или игнорировать часть данных, но он достаточно примитивный (хоть и может содержать сложные парсеры DTO) и призван просто улучшить читаемость чужого API. Это можно сделать вводя более строгие типы, конкретизируя сигнатуры методов, разделяя вызовы или группируя по смыслу. Важно не пытаться тут сразу транслировать удаленные вызовы в наши доменные сущности. Если у нас уже есть достаточно хорошая реализация чужого API, то дополнительный фасад не требуется.
• Чтобы совместить фасад с нашим интерфейсом, мы делаем адаптер. Он будет трансформировать вызовы выраженные в наших терминах (понятные нашей БЛ) в вызовы в терминах чужого сервиса (понятные фасаду или чужому коду). При этом на одно обращение к нему, он может делать несколько вызовов чужого API, если это требуется.
• Так как кроме самих вызовов необходимо так же менять форму данных, это часто удобно вынести в отдельные трансляторы. В качестве них могут быть использованы дополнительные объекты или просто методы адаптера.
Пример из практики в комментарии
Дополнительные материалы:
• https://martinfowler.com/articles/patterns-legacy-displacement/legacy-mimic.html
• https://adaptix.readthedocs.io/en/latest/conversion/tutorial.html
С
Советы разработчикам (python и не только)
14.03.2025 08:53 · 👁 16.4K
Словари
Словари есть во многих языках программирования и предназначены для поиска и хранения значений, связанных с некоторыми ключами. В простом случае словарь можно представить как список пар ключ-значение, но ключи уникальны.
Основная задача словаря - обеспечивать доступ по ключу, поэтому мы хотим, чтобы он делал это эффективно. Если хранить пары как обычный неупорядоченный список, для поиска придется перебирать его каждый раз, но можно сделать быстрее.
Одна из возможных реализаций словаря - использование сбалансированных деревьев поиска (обычно красно-черных). Принцип работы заключается в том, что для каждого узла дерева все что в правой ветке - больше него, а всё что в левой - меньше. Если ветки не сильно разной длины, мы можем достаточно быстро так найти нужный элемент. Единственное, что требуется от ключей - возможность их сравнивать на <,> и =. Результат сравнения не должен меняться в процессе жизни дерева, чтобы порядок сохранялся. Примеры: std::map в C++, TreeMap в Java, SortedDictionary в C#
Более распространенный вариант - использование хэш таблиц. В этом случае мы храним элементы в списке, но имеем способы быстро найти место в списке по ключу. Для этого используется хэш функция. Примеры: dict в Python, std::unordered_map в C++, HashMap в Java
Хэш-функция - это такая функция, которая из кучи возможных данных умеет делать числа ограниченного размера. Для одинаковых данных числа должны получаться одинаковые, для разных - как получится. Так как вариантов исходных данных заведомо больше чем чисел в ограниченном диапазоне, то повторения (коллизии) неизбежны.
Таким образом, когда нам надо узнать, где находится элемент в хэш-таблице, мы ключ превращаем в число (сначала с помощью хэширования, затем обрезая до нужного размера). А так как для разных ключей числа могут повторяться, мы дальше дополнительно проверяем тот ключ нашли или нет. Чтобы обработать несколько ключей, мы либо храним их в дополнительном маленьком списке, либо специальным алгоритмом пересчитываем индекс и прыгаем дальше. Желательно, чтобы такие повторения происходили не очень часто, но сами по себе они неизбежны.
Таким образом, для работы хэш-таблицы нам нужно, чтобы:
• для каждого ключа было можно посчитать число-хэш
• хэш у одинаковых ключей был одинаков
• хэш ключа не менялся и не ломал этим логику расположения элементов
Можно попытаться упростить эти требования до того, чтобы ключи были неизменяемы, но в целом это не требуется. Как правило, нужно чтобы не менялась только та часть данных ключа, которая учитывается при расчете хэша.
В Python для многих объектов равенство определено не на основе данных, а по факту, что это один экземпляр, поэтому и хэширование для них может быть безопасно определено на основе адреса в памяти.
• Для некоторых встроенных типов, таких как function, type или генераторов, разные экземпляры никогда не равны и хэш определен тривиально.
• Если вы пишете кастомный класс, по дефолту у него есть __eq__ и __hash__ на основе "адреса". Но если вы самостоятельно определяете сравнение в своём классе, то автоматический хэш пропадает.
• Для других типов, таких как tuple и list, равенство определяется содержимым, поэтому и хэш основам на нем. А если данные могут меняться, то стабильный хэш получить для таких типов невозможно.
Дополнительные материалы:
• https://habr.com/ru/articles/830026/
• https://habr.com/ru/articles/555404/
• https://ru.wikipedia.org/wiki/Сюръекция
С
Советы разработчикам (python и не только)
08.03.2025 11:49 · 👁 14.3K
Dependency Inversion Principle
Принцип инверсии зависимостей (DIP) часто путают с техникой внедрения зависимостей (DI), но это разные вещи, служащие разным целям. Начнем с самой инверсии.
Представим ситуацию: у нас есть компонент А и ему для работы нужен компонент D. Например, для обработки данных нам надо их загрузить из БД. Это прямая зависимость: компонент А знает о компоненте D, а компонент D не знает о компоненте А. Под знанием я имею в виду использование в коде типов, импортов, да и в целом проектирование одного куска кода исходя из того, как устроен второй.
Инверсия этой зависимости получится когда компонент А перестанет знать о компоненте D, а вместо этого компонент D станет знать о компоненте А. То есть обработка данных не знает о том, как они загружаются, но код загрузки данных может знать, что их будут обрабатывать. Держим в голове, что D все ещё должен использоваться внутри А - мы не меняем логику кода, мы только работаем с тем, как устроена зависимость.
Чтобы добиться такой инверсии, мы выделяем требования компонента А к зависимости. Это его часть. Они часто могут быть выражены в виде интерфейса или абстрактного класса (B). В свою очередь, компонент D будет реализовывать эти требования. После этих манипуляций мы получаем, что компонент А ничего не знает о настоящем D. В свою очередь, D начинает знать о требованиях А. В рамках примера мы получаем интерфейс "Загрузчик данных" и реализацию "ЗагрузчикДанныхSQL".
• Было: А -знает-> D. D не знает об А. А использует D.
• Стало: А не знает D. D -знает-> о требованиях А. А все ещё использует D, но думает только о B.
Обратите внимание, что я говорю о компонентах - это могут быть модули, группы классов или даже функции. Так же нигде не было речи о том, как D будет подставлен вместо B, мы можем использовать любые подходы для организации этого, но конечно же DI зачастую удобнее.
Непосредственно сам принцип инверсии зависимостей говорит, что более абстрактные части код не должны знать о более конкретных, более универсальные о частных, более высокоуровневые о низкоуровневых. Иногда это выполняется само по себе, но иногда мы для этого специально инвертируем зависимости.
Например, коду расчета заработной платы может понадобиться выводить результат на экран. И, скорее всего, коду расчета совершенно не важно, какое у экрана разрешение и вообще, действительно ли это настоящий экран. На лицо зависимость, которую мы можем инвертировать. При этом, у нас есть код, который непосредственно занимается выводом на экран, у него есть зависимость от самого экрана, однако они на одном уровне и инверсия не требуется.
Цели этого - борьба со сложностью программы, облегчение тестирования, увеличение гибкости системы, изоляция будущих изменений, упрощение переиспользования кода. Имея понятные абстракции, мы можем быстрее понять, что делает код, не вдаваясь в детали реализации зависимостй. Мы можем подменять реализации зависимостей, если нам это понадобится, мы с большей вероятностью не сломаем другой код, если не нарушаем контракт, в отличие от ситуации, когда контракта нет.
У инверсии зависимости есть цена. Если без её использования мы могли сразу перейти к реализации и увидеть, как всё устроено, с теперь эту реализацию ещё надо поискать. Если абстракция выделена плохо, недостаточно полно описывает требования или наоборот загрязнена ненужными деталями, мы платим цену DIP, но не получаем его преимуществ.
Дополнительные материалы:
• https://martinfowler.com/articles/dipInTheWild.html
• https://blog.cleancoder.com/uncle-bob/2016/01/04/ALittleArchitecture.html
• https://martinfowler.com/bliki/TestDouble.html
С
Советы разработчикам (python и не только)
20.02.2025 09:34 · 👁 15.3K
float и Decimal
Вас никогда не удивляло, что 0.1 + 0.2 != 0.3? Почему float считает с погрешностями, и всем норм?
Дело в том, что 0.1 выглядит как
0 0111111101 11001100110011001100110011001100110011001100110011010.
Где:
• 0 обозначает знак + (и 1 обозначает -)
• 0111111101 обозначает exponent, равную 0^10 + 2^9 + 2^8 + 2^7 + 2^6 + итд = 1019. Вычтем 1023 (размерность double) и получим итоговое значение: 1019 - 1023 = 4
• 11001100110011001100110011001100110011001100110011010 обозначет "significand" или "мантису", которая равна: 2^-exp + 2^-exp-1 + 2^-exp-2 + итд ~= 0.1
Вот так мы можем примерно представить 0.1 в виде float. Примерно – потому что все вычисления идут с погрешностью. Мы можем проверить данное утверждение, добавив погрешность вручную:
>>> assert 0.1 + 2.220446049250313e-18 == 0.1
Значение внешне не изменилось при добавлении погрешности. Посмотрим на sys.float_info.epsilon, который устанавливает необходимый порог для минимальных отличий 1.0 от следующего float числа.
>>> import sys
>>> sys.float_info.epsilon
2.220446049250313e-16
>>> assert 1.0 + sys.float_info.epsilon > 1.0
>>> assert 1.0 + 2.220446049250313e-17 == 1.0 # число меньше epsilon
Как конкретно будет выглядеть 0.1? А вот тут нам уже поможет Decimal для отображения полного числа в десятичной системе:
>>> decimal.Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
И вот ответ про 0.1 + 0.2, полное демо с битиками:
>>> decimal.Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> decimal.Decimal(0.2)
Decimal('0.200000000000000011102230246251565404236316680908203125')
>>> decimal.Decimal(0.1 + 0.2)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> decimal.Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
Числа не равны друг другу, потому что их разница больше предельной точности float. А сам Decimal может использовать любую точность под задачу.
>>> from decimal import Decimal, getcontext
>>> getcontext().prec = 6
>>> Decimal(1) / Decimal(7)
Decimal('0.142857')
>>> getcontext().prec = 28
>>> Decimal(1) / Decimal(7)
Decimal('0.1428571428571428571428571429')
Но и Decimal не может в абсолютную точность, потому что есть в целом невыразимые в десятичной системе числа, такие как math.pi, ⅓, тд. С чем-то из них может помочь fractions.Fraction для большей точности, но от существования иррациональных чисел никуда не деться.
Почему всем норм, что у нас с float такие погрешности в вычислениях? Потому что во многих задачах абсолютная точность недостижима и не имеет смысла. Благодаря плавающей точке мы можем хранить как очень большие, так и очень маленькие числа без существенных затрат памяти. А ещё float - очень быстрый. В том числе за счет аппаратного ускорения.
» pyperf timeit -s 'a = 0.1; b = 0.2' 'a + b'
.....................
Mean +- std dev: 8.75 ns +- 0.2 ns
» pyperf timeit -s 'import decimal; a = decimal.Decimal("0.1"); b = decimal.Decimal("0.2")' 'a + b'
.....................
Mean +- std dev: 27.7 ns +- 0.1 ns
Разница в 3 раза.
Про то, как устроен float внутри – рассказывать не буду. У Никиты Соболева недавно было большое и подробное видео на тему внутреннего устройства float. У него действительно хороший технический контент, советую подписаться: @opensource_findings
Итого
Если у вас нет требований по работе именно с десятичной записью числа (как, например, в бухгалтерии), то используйте float. Он даст достаточную точность и хорошую скорость. Если вы хотите, чтобы расчеты велись в десятичных цифрах и ваши расчеты построены так, что абсолютная точность достижима, то используйте Decimal.
Дополнительные материалы:
• https://www.youtube.com/@sobolevn/
• https://0.30000000000000004.com
• https://en.wikipedia.org/wiki/X87
• http://aco.ifmo.ru/el_books/numerical_methods/lectures/app_1.html