Статьи
October 17, 2022

Как избежать арифметических ошибок с вещественными числами в Python

Мы рассчитываем на точность при написании кода. В конце концов, работу выполняет компьютер. Тем не менее, ваши вычисления могут быть не совсем верными, а вы даже не будете подозревать об этом.

Если вы сталкивались с арифметическими ошибками при работе с числами с плавающей точкой, то вы знаете, о чём идёт речь. Рассмотрим конкретный пример:

print(1.1 * 3)  # 3.3000000000000003

Так происходит из-за того, что десятичные числа на самом деле хранятся в виде формулы и не имеют точного представления.

Давайте попробуем найти решение данной проблеме с помощью встроенной библиотеки Decimal.

Как пользоваться библиотекой Decimal

Согласно официальной документации Python:

Модуль decimal обеспечивает быстрое и корректное округление десятичных чисел с плавающей точкой.

Как нам воспользоваться этим инструментом? Начнём с импорта библиотеки. Чтобы импортировать все её компоненты будем использовать символ *.

from decimal import *

Далее мы используем конструктор Decimal() со строковым значением внутри для создания нового объекта и снова попробуем выполнить наши арифметические действия.

print(Decimal('1.1') * 3)  # 3.3

Убедитесь, что работаете именно со строковым значением. В противном случае число с плавающей точкой 1.1 будет преобразовано в Decimal-объект, и ошибка сохранится, а возможно и усугубится.

print(Decimal(1.1) * 3)  # 3.300000000000000266453525910

«Золотое правило»

В принципе, вы можете использовать Decimal-объекты таким же образом, как и любые другие числовые значения. Однако, если вы решите использовать библиотеку Decimal, помните об одном «золотом правиле»: не смешивайте Decimal-объекты с float-объектами.

Если вы будете использовать Decimal-числа и float-числа вместе, то, скорее всего, получите ошибку. В случае арифметических вычислений эти два типа данных несовместимы.

a = Decimal('1.1')
b = 2.2
c = a + b
# TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'

Ниже приведены дополнительные советы и рекомендации по использованию Decimal().

Используйте .quantize() для округления

Передайте в метод .quantize() Decimal-объект с имеющимся количеством знаков после запятой. Это бывает особенно полезно при работе с денежными единицами. В примере ниже мы округлим значение до двух знаков после запятой.

a = Decimal('1.123456789')
b = a.quantize(Decimal('1.00'))
print(b)  # 1.12

Для настройки точности округления используйте getcontext()

Количество цифр после запятой можно настроить, изменив текущий контекст.

Контекст вычислений определяет точность, правила округления, ограничения на экспоненты, флаги результатов операций и средства вызова исключений.

Сначала давайте взглянем на то, как выглядит текущий контекст, а затем продемонстрируем, что происходит, когда мы вносим в него изменения.

from decimal import *

print(getcontext())
"""
Context(prec=28, 
rounding=ROUND_HALF_EVEN, 
Emin=-999999, Emax=999999, 
capitals=1, clamp=0, flags=[], 
traps=[InvalidOperation, DivisionByZero, Overflow])
"""
print(Decimal(1) / Decimal(3))
# 0.3333333333333333333333333333
getcontext().prec = 4
print(Decimal(1)/Decimal(3))
# 0.3333

Будьте осторожны с операциями взятия остатка от деления при отрицательных числах

Операция % возвращает остаток от операции деления. Обычно при использовании отрицательного числа сохраняется знак делителя.

print((-7) % 4)  # 1
print(7 % (-4))  # -1

В случае же применения Decimal-объекта сохраняется знак делимого.

print(Decimal(-7) % 4)  # -3
print(7 % Decimal(-4))  # 3

👉🏻Подписывайтесь на PythonTalk в Telegram 👈🏻

👨🏻‍💻Чат PythonTalk в Telegram💬

🍩 Поддержать канал 🫶

Источник: medium.com
Перевод и адаптация: Ирина Гарибян