2025, Oct 07 01:16
Округление почти нулевых float в Decimal: роль context.Emin
Почему create_decimal_from_float в Python Decimal не прижимает почти нули к 0: экспонента идет раньше точности. Решение — ограничить Emin (context.Emin).
При преобразовании чисел с плавающей запятой в Decimal легко предположить, что точность контекста округлит значения именно так, как вы ожидаете. С create_decimal_from_float() это в целом работает — пока не появляются числа, почти равные нулю. Тогда вместо округления до нуля значение может показаться в научной нотации как крошечная ненулевая величина. Если вы когда‑нибудь пытались превратить что‑то вроде sin(180°) в Decimal и удивлялись оставшемуся 1e-18, это как раз тот случай (Python 3.10).
Демонстрация проблемы
Точность установлена на шесть значащих цифр. Обычное значение округляется как ожидается, а почти нулевое — не прижимается к 0:
>>> import decimal
>>> policy = decimal.getcontext()
>>> policy.prec = 6
>>> policy.create_decimal_from_float(0.123456789123456789)
Decimal('0.123457')
>>> policy.create_decimal_from_float(0.000000000000000001)
Decimal('1.00000E-18')
Что на самом деле происходит
Для обычного конструктора Decimal(float_value) документация говорит прямо:
Значимость нового Decimal определяется только числом введённых цифр. Точность контекста и округление вступают в действие только во время арифметических операций.
create_decimal_from_float() ведёт себя иначе. Он использует активный десятичный контекст и применяет точность и округление уже на этапе создания. Однако есть ключевая деталь: сначала учитывается экспонента, а уже затем — точность. Значения, близкие к нулю, получают отрицательную экспоненту, и только потом округляются цифры. Поэтому крошечный ввод показывается как что‑то вроде 1.00000E-18, а не округляется до 0 при текущей точности.
Минимальная иллюстрация с точностью в четыре значащих цифры:
>>> import decimal
>>> rules = decimal.getcontext()
>>> rules.prec = 4
>>> rules.create_decimal_from_float(0.000000000123456789)
Decimal('1.235E-10')
Исправление: ограничьте экспоненту (Emin)
Если вы хотите «срезать» все почти нулевые экспоненты и прижать представление чисел текущей точностью, ограничьте диапазон экспоненты в контексте. Конкретно: установите Emin в 0, чтобы не использовать экспоненциальную запись:
>>> import decimal
>>> conf = decimal.getcontext()
>>> conf.prec = 4
>>> conf.Emin = 0
>>> conf.create_decimal_from_float(0.000000000123456789)
Decimal('0.000')
Принудительно устанавливая Emin в 0, вы не позволяете пути создания представить крошечные величины с отрицательной экспонентой; значение затем округляется с заданной точностью и схлопывается до нуля.
Почему это важно
Переход между двоичными float и Decimal — обычная практика при ужесточении численного поведения в финансовом или научном коде. Пограничный случай около нуля может незаметно разойтись с ожиданиями по округлению, оставив крошечные ненулевые значения там, где вы рассчитывали на нули. Понимание того, что в десятичном контексте экспонента обрабатывается до применения точности — и что это можно контролировать через Emin — помогает согласовать конвертацию с вашей политикой округления.
Вывод
При создании Decimal из float в заданном контексте одной точности недостаточно, чтобы микроскопические значения превращались в нули, потому что сначала обрабатывается экспонента. Если хотите, чтобы почти нулевые входы «сходили на нет» при текущей точности, перед вызовом create_decimal_from_float() установите context.Emin = 0. Это удержит числа от научной нотации для малых величин и согласует результат с предполагаемым округлением.
Статья основана на вопросе на StackOverflow от Qwerty-uiop и ответе от jsbueno.