Decimal зависит от чужой настройки: изолировать контекст расчёта
Модуль отчётности временно уменьшил точность Decimal. Позже расчёт комиссии в том же процессе стал возвращать другое число или выбрасывать InvalidOperation. Входные суммы по-прежнему создаются из строк, поэтому привычный совет отказаться от float здесь уже выполнен.
Точное представление не задаёт правила операции
Decimal из строки сохраняет десятичное значение, однако арифметические операции выполняются с настройками контекста. В нём задаются точность, округление и обработка сигналов. Если функция рассчитывает на внешние настройки, её поведение зависит от того, какой код исполнялся рядом. Это особенно трудно заметить в тестах, где расчёт запускается в чистом процессе.
Для прикладного расчёта опишите правило словами. Здесь комиссия равна сумме, умноженной на ставку; округляется окончательная комиссия до копейки по ROUND_HALF_UP. Промежуточное произведение не округляется до денежного масштаба заранее. Пограничное значение 0.005 должно дать 0.01. Такое утверждение позволяет обсуждать требования до выбора конкретного API.
Точность контекста обозначает число значащих цифр, а не число знаков после запятой. Поэтому prec=2 не означает работу в копейках и не подходит для произвольной суммы. Масштаб результата задаётся quantize с Decimal("0.01"). Рабочую точность выбирают по диапазону сумм и количеству операций; пример ограничивает вход двумя денежными знаками, ставку шестью дробными и сумму двенадцатью целыми разрядами.
Задать собственный контекст
localcontext создаёт временную область настроек и восстанавливает прежнюю после выхода. Чтобы не наследовать случайные ловушки и ограничения внешнего контекста, передаём собственный Context. В нём явно фиксируем рабочую точность и округление. Внешний код в тесте намеренно работает с другой точностью, однако результат комиссии должен соответствовать контракту функции.
Здесь проверка формата ограничена диапазоном и масштабом: это важно для выбранной точности. У реального входного API добавьте проверку типа, конечности и допуска отрицательных сумм согласно предметным правилам. Не пропускайте NaN в денежный поток. После вычисления функция возвращает Decimal; превращение в JSON должно сохранять согласованный десятичный формат, например строку.
from decimal import Decimal, Context, localcontext, ROUND_HALF_UP, ROUND_DOWN
CALCULATION = Context(prec=32, rounding=ROUND_HALF_UP)
CENT = Decimal("0.01")
def commission(amount, rate):
if not amount.is_finite() or not rate.is_finite():
raise ValueError("finite values required")
if not (Decimal("0") <= amount < Decimal("1e12")):
raise ValueError("amount outside contract")
if not (Decimal("0") <= rate <= Decimal("1")):
raise ValueError("rate outside contract")
if amount.as_tuple().exponent < -2 or rate.as_tuple().exponent < -6:
raise ValueError("too many decimal places")
with localcontext(CALCULATION):
return (amount * rate).quantize(CENT)
with localcontext() as outer:
outer.prec = 4
outer.rounding = ROUND_DOWN
small = commission(Decimal("0.10"), Decimal("0.05"))
large = commission(Decimal("123456.78"), Decimal("0.015"))
assert small == Decimal("0.01")
assert large == Decimal("1851.85")
assert outer.prec == 4 and outer.rounding == ROUND_DOWN
print(small, large, outer.prec)Проверять правило на границах
Обычная сумма редко различает два способа округления. Половина копейки делает правило наблюдаемым сразу. Добавьте соседние значения ниже и выше границы, нулевую ставку и максимальный допустимый вход. Для возвратов с отрицательными суммами сначала согласуйте симметрию округления и ограничение диапазона: нельзя просто снять проверку и считать, что финансовый смысл автоматически сохранится.
Если комиссия состоит из нескольких строк, уточните, округляется каждая строка или общий итог. Сумма округлённых результатов может отличаться от округлённой суммы. Это требование влияет на место quantize, а не на выбор количества знаков в repr. В функции из примера одна сумма образует одну комиссию; пакетный расчёт должен явно сохранить или изменить это правило.
Изоляция контекста полезна и для повторяемости тестов: выполните функцию внутри нескольких заведомо разных внешних контекстов. Проверяйте возвращённое значение и восстановление настроек после исключения. Так тест защищает границу зависимости, а не только один красивый числовой пример. Изменение правил расчёта затем становится локальным изменением контракта функции.
Чтобы проверить место округления, возьмите две небольшие комиссии, каждая из которых находится около половины копейки. Рассчитайте их сначала как две отдельные операции, затем как одну объединённую сумму. Разница сама по себе не означает дефекта библиотеки: она показывает, что две бизнес-политики дают разные ответы. Выберите нужную и сохраните тест с конкретным ожидаемым итогом. Если расчёт получает значения из базы данных, проследите путь до Decimal: преобразование через float между чтением и функцией вернёт проблему двоичного представления. На границе полезно сохранять исходную десятичную строку либо тип числового значения драйвера. Тогда изолированный контекст действительно управляет арифметикой над согласованными исходными данными, а не над уже искажённым приближением.
Следующий шаг в практике
В открытом уроке о pytest закрепите проверки пограничных значений и исключений. Посмотрите программу продвинутого Python, чтобы перейти к параметризации и изоляции тестов. Для дальнейшей работы с менеджерами контекста и стандартной библиотекой откройте «Python для профессионалов».
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
- Python для профессионалов ↗
Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.
