Арифметика с плавающей точкой: проблемы и ограничения
Числа с плавающей точкой представлены в аппаратном обеспечении компьютера в виде дробей с основанием 2 (двоичных дробей). Например, десятичная дробь 0.625 имеет значение 6/10 + 2/100 + 5/1000, а аналогично двоичная дробь 0.101 имеет значение 1/2 + 0/4 + 1/8. Значения этих двух дробей одинаковы; единственное реальное различие в том, что первая записана в десятичной системе счисления, а вторая — в двоичной.
К сожалению, большинство десятичных дробей нельзя точно представить в виде двоичных дробей. В результате десятичные числа с плавающей точкой, которые вы вводите, как правило, лишь приближённо представляются двоичными числами с плавающей точкой, фактически хранящимися в компьютере.
Сначала эту проблему проще понять на примере десятичной системы счисления. Рассмотрим дробь 1/3. Её можно приближённо представить десятичной дробью:
0.3
или, точнее,
0.33
или, ещё точнее,
0.333
и так далее. Сколько бы цифр вы ни были готовы записать, результат никогда не будет в точности равен 1/3, а будет лишь всё точнее приближаться к 1/3.
Точно так же десятичное значение 0.1 нельзя точно представить в виде двоичной дроби, сколько бы двоичных цифр вы ни использовали. В двоичной системе счисления 1/10 — это бесконечная периодическая дробь
0.0001100110011001100110011001100110011001100110011...
Если остановиться на любом конечном числе битов, получится приближение. На большинстве современных компьютеров числа с плавающей точкой приближённо представляются двоичной дробью, в числителе которой используются первые 53 бита, начиная со старшего бита, а знаменателем служит степень двойки. В случае 1/10 двоичная дробь имеет вид 3602879701896397 / 2 ** 55 — она близка к истинному значению 1/10, но не равна ему.
Многие пользователи не замечают приближения из-за того, как отображаются значения. Python выводит лишь десятичное приближение к истинному десятичному значению двоичного приближения, сохранённого компьютером. На большинстве компьютеров, если бы Python вывел истинное десятичное значение двоичного приближения, сохранённого для 0.1, ему пришлось бы показать:
>>> 0.1 0.1000000000000000055511151231257827021181583404541015625
Это больше цифр, чем большинство людей считает полезным, поэтому Python ограничивает их количество и выводит округлённое значение:
>>> 1 / 10 0.1
Помните: хотя напечатанный результат выглядит как точное значение 1/10, фактически сохранено ближайшее представимое двоичное значение.
Интересно, что многим различным десятичным числам соответствует одно и то же ближайшее двоичное приближение. Например, числа 0.1, 0.10000000000000001 и 0.1000000000000000055511151231257827021181583404541015625 все приближённо представляются как 3602879701896397 / 2 ** 55. Поскольку все эти десятичные значения имеют одно и то же приближение, можно вывести любое из них, сохранив при этом инвариант eval(repr(x)) == x.
Раньше приглашение Python и встроенная функция repr() выбирали вариант с 17 значащими цифрами: 0.10000000000000001. Начиная с Python 3.1, Python (в большинстве систем) может выбрать самый короткий из этих вариантов и просто вывести 0.1.
Обратите внимание: это свойство двоичной арифметики с плавающей точкой. Это не ошибка Python и не ошибка вашего кода. Вы столкнётесь с тем же явлением во всех языках, поддерживающих арифметику с плавающей точкой вашего аппаратного обеспечения (хотя некоторые языки могут по умолчанию не отображать это различие или не отображать его во всех режимах вывода).
Чтобы получить более удобный для чтения результат, можно воспользоваться форматированием строк и вывести ограниченное число значащих цифр:
>>> format(math.pi, '.12g') # give 12 significant digits '3.14159265359' >>> format(math.pi, '.2f') # give 2 digits after the point '3.14' >>> repr(math.pi) '3.141592653589793'
Важно понимать, что это, по сути, иллюзия: вы просто округляете отображение истинного значения, хранящегося в компьютере.
Одна иллюзия может порождать другую. Например, поскольку 0.1 не равно в точности 1/10, сумма трёх значений 0.1 также может быть не равна в точности 0.3:
>>> 0.1 + 0.1 + 0.1 == 0.3 False
Кроме того, поскольку значение 0.1 нельзя приблизить к точному значению 1/10, а значение 0.3 — к точному значению 3/10, предварительное округление с помощью функции round() не поможет:
>>> round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1) False
Хотя числа нельзя приблизить к задуманным точным значениям, функция math.isclose() может пригодиться для сравнения неточных значений:
>>> math.isclose(0.1 + 0.1 + 0.1, 0.3) True
В качестве альтернативы для сравнения приблизительных значений можно использовать функцию round():
>>> round(math.pi, ndigits=2) == round(22 / 7, ndigits=2) True
Арифметика с двоичной плавающей точкой таит множество подобных неожиданностей. Проблема с «0.1» подробно объясняется ниже, в разделе «Погрешность представления». В статье Примеры проблем с числами с плавающей точкой приведено понятное описание принципов работы двоичной арифметики с плавающей точкой и типичных проблем, возникающих на практике. Более подробный разбор других распространённых неожиданностей см. в статье Подводные камни арифметики с плавающей точкой.
Как сказано ближе к концу этой статьи, «простых ответов не существует». И всё же не стоит чрезмерно опасаться чисел с плавающей точкой! Погрешности операций с числами Python типа float наследуются от аппаратного обеспечения, выполняющего операции с плавающей точкой, и на большинстве компьютеров для каждой операции они не превышают примерно 1 части из 2**53. Этого более чем достаточно для большинства задач, но важно помнить, что это не десятичная арифметика и что при каждой операции с числами типа float может возникнуть новая ошибка округления.
Хотя патологические случаи существуют, при обычном использовании арифметики с плавающей точкой итог обычно будет таким, как вы ожидаете, если просто округлить отображение конечных результатов до нужного количества десятичных цифр. Обычно достаточно str(); для более точного управления см. спецификаторы формата метода str.format() в разделе Синтаксис форматирования строк.
Для задач, требующих точного десятичного представления, попробуйте модуль decimal, реализующий десятичную арифметику, подходящую для бухгалтерских и высокоточных вычислений.
Другую форму точной арифметики поддерживает модуль fractions, реализующий арифметику на основе рациональных чисел (поэтому такие числа, как 1/3, можно представить точно).
Если вы часто выполняете операции с числами с плавающей точкой, обратите внимание на пакет NumPy и многие другие пакеты для математических и статистических вычислений, предоставляемые проектом SciPy. См. <https://scipy.org>.
В Python есть инструменты, которые могут помочь в тех редких случаях, когда вам действительно нужно узнать точное значение числа типа float. Метод float.as_integer_ratio() представляет значение числа типа float в виде дроби:
>>> x = 3.14159 >>> x.as_integer_ratio() (3537115888337719, 1125899906842624)
Поскольку это точное отношение, с его помощью можно без потерь восстановить исходное значение:
>>> x == 3537115888337719 / 1125899906842624 True
Метод float.hex() представляет число типа float в шестнадцатеричной системе счисления (с основанием 16), также показывая точное значение, сохранённое на компьютере:
>>> x.hex() '0x1.921f9f01b866ep+1'
Это точное шестнадцатеричное представление можно использовать для восстановления значения float:
>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True
Поскольку представление точное, оно полезно для надёжного переноса значений между разными версиями Python (независимо от платформы), а также для обмена данными с другими языками, поддерживающими тот же формат (например, Java и C99).
Ещё один полезный инструмент — функция sum(), которая помогает уменьшить потерю точности при суммировании. Она использует расширенную точность при промежуточных шагах округления, когда значения добавляются к текущей сумме. Это может повысить общую точность и не дать ошибкам накапливаться настолько, чтобы повлиять на итог:
>>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0 False >>> sum([0.1] * 10) == 1.0 True
Функция math.fsum() идёт дальше и отслеживает все «потерянные цифры» при добавлении значений к текущей сумме, благодаря чему результат округляется только один раз. Она работает медленнее, чем sum(), но обеспечивает большую точность в редких случаях, когда большие по модулю значения в основном взаимно компенсируются и итоговая сумма оказывается близкой к нулю:
>>> arr = [-0.10430216751806065, -266310978.67179024, 143401161448607.16, ... -143401161400469.7, 266262841.31058735, -0.003244936839808227] >>> float(sum(map(Fraction, arr))) # Exact summation with single rounding 8.042173697819788e-13 >>> math.fsum(arr) # Single rounding 8.042173697819788e-13 >>> sum(arr) # Multiple roundings in extended precision 8.042178034628478e-13 >>> total = 0.0 >>> for x in arr: ... total += x # Multiple roundings in standard precision ... >>> total # Straight addition has no correct digits! -0.0051575902860057365
15.1. Погрешность представления
В этом разделе подробно рассматривается пример с «0.1» и показывается, как самостоятельно выполнить точный анализ подобных случаев. Предполагается базовое знакомство с двоичным представлением чисел с плавающей точкой.
Погрешность представления означает, что некоторые (на самом деле — большинство) десятичные дроби нельзя точно представить в виде двоичных дробей (с основанием 2). Именно поэтому Python (а также Perl, C, C++, Java, Fortran и многие другие языки) часто не выводит ожидаемое вами точное десятичное число.
Почему так происходит? 1/10 нельзя точно представить в виде двоичной дроби. Начиная как минимум с 2000 года почти все компьютеры используют двоичную арифметику с плавающей точкой IEEE 754, а почти на всех платформах числа Python типа float соответствуют значениям IEEE 754 binary64 («двойной точности»). Значения IEEE 754 binary64 содержат 53 бита точности, поэтому при вводе компьютер стремится преобразовать 0.1 в ближайшую возможную дробь вида J/2**N, где J — целое число, содержащее ровно 53 бита. Перепишем
1 / 10 ~= J / (2**N)
в виде
J ~= 2**N / 10
и вспомним, что J содержит ровно 53 бита (это >= 2**52, но не < 2**53). Наилучшее значение для N — 56:
>>> 2**52 <= 2**56 // 10 < 2**53 True
Иными словами, 56 — единственное значение N, при котором J содержит ровно 53 бита. Тогда наилучшее возможное значение J — это округлённое частное:
>>> q, r = divmod(2**56, 10) >>> r 6
Поскольку остаток больше половины от 10, лучшее приближение получается при округлении вверх:
>>> q+1 7205759403792794
Следовательно, наилучшее возможное приближение 1/10 в формате двойной точности IEEE 754 равно:
7205759403792794 / 2 ** 56
Разделив числитель и знаменатель на два, получим дробь:
3602879701896397 / 2 ** 55
Обратите внимание: поскольку мы округлили вверх, это значение немного больше 1/10; если бы мы не округляли вверх, частное было бы немного меньше 1/10. Но ни в одном случае оно не может быть в точности равно 1/10!
Таким образом, компьютер никогда не «видит» 1/10: он видит точную дробь, указанную выше, — наилучшее приближение, которое можно получить в формате двойной точности IEEE 754:
>>> 0.1 * 2 ** 55 3602879701896397.0
Если умножить эту дробь на 10**55, можно увидеть значение с точностью до 55 десятичных цифр:
>>> 3602879701896397 * 10 ** 55 // 2 ** 55 1000000000000000055511151231257827021181583404541015625
то есть точное число, хранящееся в компьютере, равно десятичному значению 0.1000000000000000055511151231257827021181583404541015625. Вместо вывода полного десятичного значения многие языки (включая старые версии Python) округляют результат до 17 значащих цифр:
>>> format(0.1, '.17f') '0.10000000000000001'
Модули fractions и decimal упрощают такие вычисления:
>>> from decimal import Decimal
>>> from fractions import Fraction
>>> Fraction.from_float(0.1)
Fraction(3602879701896397, 36028797018963968)
>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)
>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> format(Decimal.from_float(0.1), '.17')
'0.10000000000000001'
© 2001–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/tutorial/floatingpoint.html