Использование компьютера для выполнения довольно сложной математики - одна из причин, по которой эта машина была изначально разработана. Пока в вычислениях участвуют исключительно целые числа и сложения, вычитания и умножения, все в порядке. Как только в игру вступают числа с плавающей запятой или дроби, а также деления, это чрезвычайно усложняет все дело.
Как обычный пользователь, мы не в полной мере осознаем эти проблемы, которые происходят за кулисами и могут привести к довольно неожиданным и, возможно, неточным результатам для наших расчетов. Как разработчики, мы должны принять во внимание соответствующие меры, чтобы компьютер работал правильно.
В нашей повседневной жизни мы используем десятичную систему, основанную на числе 10. Компьютер использует двоичную систему с основанием 2, а внутри он хранит и обрабатывает значения как последовательность единиц и нулей. Ценности, с которыми мы работаем, должны постоянно трансформироваться между двумя представлениями. Как объяснено в документации Python :
... большинство десятичных дробей не могут быть представлены точно как двоичные дроби. Следствием этого является то, что, как правило, вводимые вами десятичные числа с плавающей запятой только аппроксимируются двоичными числами с плавающей запятой, фактически сохраненными в машине.
Такое поведение приводит к удивительным результатам в простых дополнениях, как показано здесь:
Листинг 1: Неточности с числами с плавающей запятой
>>> s = 0.3 + 0.3 + 0.3
>>> s
0.8999999999999999
Как вы можете видеть здесь, результат неточный, так как он должен быть 0,9.
В листинге 2 показан аналогичный случай форматирования числа с плавающей запятой для 17 десятичных разрядов.
Листинг 2: Форматирование числа с плавающей запятой
>>> format(0.1, '.17f')
'0.10000000000000001'
Как вы, возможно, узнали из приведенных выше примеров, работа с числами
с плавающей запятой немного сложна и требует дополнительных мер для
достижения правильного результата и минимизации вычислительных ошибок.
Округление значения может решить по крайней мере некоторые проблемы.
Одна из возможностей - встроенная round()
(подробнее о ее
использовании см. Ниже):
Листинг 3: Расчет с округленными значениями
>>> s = 0.3 + 0.3 + 0.3
>>> s
0.8999999999999999
>>> s == 0.9
False
>>> round(0.9, 1) == 0.9
True
В качестве альтернативы вы можете работать с математическим модулем или явно работать с дробями, хранящимися как два значения (числитель и знаменатель) вместо округленных, довольно неточных значений с плавающей запятой.
Чтобы сохранить такие значения, в игру вступают два модуля Python: десятичная и дробная (см. Примеры ниже). Но сначала давайте подробнее рассмотрим термин «округление».
Что такое округление?
В двух словах процесс округления означает:
... заменяя [значение] другим числом, которое примерно равно исходному, но имеет более короткое, простое или более явное представление.
[Источник: https://en.wikipedia.org/wiki/Rounding]{.small}
По сути, он увеличивает неточность точно рассчитанного значения, сокращая его. В большинстве случаев это делается путем удаления цифр после десятичной точки, например от 3,73 до 3,7, от 16,67 до 16,7 или от 999,95 до 1000.
Такое сокращение выполняется по нескольким причинам - например, для экономии места при сохранении значения или просто для удаления неиспользуемых цифр. Кроме того, устройства вывода, такие как аналоговые дисплеи или часы, могут отображать вычисленное значение только с ограниченной точностью и требуют скорректированных входных данных.
В общем, для округления применяются два довольно простых правила, вы можете помнить их еще со школы. Цифры от 0 до 4 ведут к округлению в меньшую сторону, а числа от 5 до 9 - к округлению в большую сторону. В таблице ниже показаны варианты использования.
| original value | rounded to | result |
|----------------|--------------|--------|
| 226 | the ten | 230 |
| 226 | the hundred | 200 |
| 274 | the hundred | 300 |
| 946 | the thousand | 1,000 |
| 1,024 | the thousand | 1,000 |
| 10h45m50s | the minute | 10h45m |
Методы округления
Математики разработали множество различных методов округления, чтобы решить проблему округления. Это включает в себя простое усечение, округление вверх, округление в меньшую сторону, округление до половины вверх, округление до половины в меньшую сторону, а также округление половины от нуля и округление половины до четного.
Например, округление от нуля до половины применяется Европейской комиссией по экономическим и финансовым вопросам при конвертации валют в евро. Некоторые страны, такие как Швеция, Нидерланды, Новая Зеландия и Южная Африка, следуют правилу под названием «округление денежных средств», «округление пенни» или «округление шведского языка».
[Округление наличных денег] происходит, когда минимальная расчетная единица меньше наименьшего физического достоинства валюты. Сумма, подлежащая выплате за транзакцию с наличными, округляется до ближайшего кратного значения минимальной доступной денежной единицы, тогда как транзакции, оплаченные другими способами, не округляются.
[Источник: https://en.wikipedia.org/wiki/Cash_rounding]{.small}
В Южной Африке с 2002 года округление наличных денег производится до ближайших 5 центов. Обычно такое округление не применяется к электронным безналичным платежам.
Напротив, округление от половины до четного является стратегией по
умолчанию для Python,
Numpy и
Pandas и
используется встроенной round()
о которой уже упоминалось ранее. Он
относится к категории методов округления до ближайшего и известен также
как конвергентное округление, округление статистики, округление по
голландскому языку, округление по Гауссу, округление по нечетным-четным
и округление банкиров. Этот метод определен в IEEE
754 и работает
таким образом, что «если дробная часть x
равна 0,5, то y
является
ближайшим к x
четным целым числом». Предполагается, что «вероятности
связи в наборе данных при округлении в меньшую или большую сторону
равны», что обычно и имеет место на практике. Хотя эта стратегия не
полностью совершенна, она дает заметные результаты.
В таблице ниже приведены практические примеры округления для этого метода:
| original value | rounded to |
|----------------|------------|
| 23.3 | 23 |
| 23.5 | 24 |
| 24.0 | 24 |
| 24.5 | 24 |
| 24.8 | 25 |
| 25.5 | 26 |
Функции Python
Python имеет встроенную функцию round()
которая в нашем случае очень
полезна. Он принимает два параметра - исходное значение и количество
цифр после десятичной точки. В листинге ниже показано использование
метода для одной, двух и четырех цифр после десятичной точки.
Листинг 4: Округление с указанным количеством цифр
>>> round(15.45625, 1)
15.5
>>> round(15.45625, 2)
15.46
>>> round(15.45625, 4)
15.4563
Если вы вызываете эту функцию без второго параметра, значение округляется до полного целого числа.
Листинг 5: Округление без указанного количества цифр
>>> round(0.85)
1
>>> round(0.25)
0
>>> round(1.5)
2
Округленные значения подходят, если вам не требуются абсолютно точные результаты. Имейте в виду, что сравнение округленных значений также может быть кошмаром. Это станет более очевидным в следующем примере - сравнении округленных значений на основе предварительного и последующего округления.
Первый расчет в листинге 6 содержит предварительно округленные значения и описывает округление перед сложением значений. Второй расчет содержит итоговую сумму после округления, что означает округление после суммирования. Вы заметите, что результат сравнения другой.
Листинг 6. Предварительное округление и последующее округление
>>> round(0.3, 10) + round(0.3, 10) + round(0.3, 10) == round(0.9, 10)
False
>>> round(0.3 + 0.3 + 0.3, 10) == round(0.9, 10)
True
Модули Python для вычислений с плавающей запятой
Есть четыре популярных модуля, которые помогут вам правильно работать с
числами с плавающей запятой. Сюда входят math
модуль, модуль Numpy
,
decimal
модуль и модуль fractions
math
модуль сосредоточен на математических константах, операциях с
плавающей запятой и тригонометрических методах. Модуль Numpy
описывает
себя как «фундаментальный пакет для научных вычислений» и известен своим
разнообразием методов работы с массивами. Модуль decimal
охватывает
десятичную арифметику с фиксированной и плавающей запятой, а fractions
имеет дело, в частности, с рациональными числами.
Во-первых, мы должны попытаться улучшить расчет из листинга 1 . Как
показано в листинге 7 , после импорта math
модуля мы можем получить
доступ к методу fsum()
который принимает список чисел с плавающей
запятой. Для первого расчета нет разницы между встроенным методом
sum()
fsum()
из math
модуля, но для второго - это так, и он
возвращает правильный результат, которого мы ожидали. Точность зависит
от базового алгоритма IEEE 754.
Листинг 7: Вычисления с плавающей запятой с помощью math
модуля
>>> import math
>>> sum([0.1, 0.1, 0.1])
0.30000000000000004
>>> math.fsum([0.1, 0.1, 0.1])
0.30000000000000004
>>> sum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
0.9999999999999999
>>> math.fsum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
1.0
Во-вторых, давайте посмотрим на модуль Numpy
Он поставляется с
методом around
(),
который округляет значения, предоставленные в виде массива. Он
обрабатывает отдельные значения так же, как метод round()
по
умолчанию.
Для сравнения значений Numpy
предлагает метод equal()
. Как и
around()
он принимает отдельные значения, а также списки значений (так
называемые векторы) для обработки. В листинге 8 показано сравнение
отдельных значений, а также округленных значений. Наблюдаемое поведение
очень похоже на ранее показанные методы.
Листинг 8: Сравнение значений с использованием метода equal из модуля
Numpy
>>> import numpy
>>> print (numpy.equal(0.3, 0.3))
True
>>> print (numpy.equal(0.3 + 0.3 + 0.3 , 0.9))
False
>>> print (numpy.equal(round(0.3 + 0.3 + 0.3) , round(0.9)))
True
Вариант третий - decimal
модуль. Он предлагает точное десятичное
представление и сохраняет значащие цифры. По умолчанию точность
составляет 28 цифр, и вы можете изменить это значение на число, которое
будет настолько большим, насколько это необходимо для вашей проблемы. В
листинге 9 показано, как использовать точность до 8 цифр.
Листинг 9: Создание десятичных чисел с помощью модуля decimal
>>> import decimal
>>> decimal.getcontext().prec = 8
>>> a = decimal.Decimal(1)
>>> b = decimal.Decimal(7)
>>> a / b
Decimal('0.14285714')
Теперь сравнение значений с плавающей запятой стало намного проще и привело к желаемому результату.
Листинг 10: Сравнение с использованием decimal
модуля
>>> import decimal
>>> decimal.getcontext().prec = 1
>>> a = decimal.Decimal(0.3)
>>> b = decimal.Decimal(0.3)
>>> c = decimal.Decimal(0.3)
>>> a + b + c
Decimal('0.9')
>>> a + b + c == decimal.Decimal('0.9')
True
Модуль decimal
также имеет метод округления значений - quantize
()
. Стратегия округления по умолчанию - округление от половины до четного,
и при необходимости ее также можно изменить на другой метод. В листинге
11 показано использование метода quantize()
. Обратите внимание, что
количество цифр указывается с использованием десятичного значения в
качестве параметра.
Листинг 11: Округление значения с помощью quantize()
>>> d = decimal.Decimal(4.6187)
>>> d.quantize(decimal.Decimal("1.00"))
Decimal('4.62')
И последнее, но не менее важное: мы рассмотрим модуль fractions
Этот
модуль позволяет обрабатывать значения с плавающей запятой как дроби,
например 0.3
как 3/10. Это упрощает сравнение значений с плавающей
запятой и полностью исключает округление значений. В листинге 12
показано, как использовать модуль дробей.
Листинг 12: Хранение и сравнение значений с плавающей запятой как дробей
>>> import fractions
>>> fractions.Fraction(4, 10)
Fraction(2, 5)
>>> fractions.Fraction(6, 18)
Fraction(1, 3)
>>> fractions.Fraction(125)
Fraction(125, 1)
>>> a = fractions.Fraction(6, 18)
>>> b = fractions.Fraction(1, 3)
>>> a == b
True
Кроме того, два модуля decimal
fractions
и дроби можно
комбинировать, как показано в следующем примере.
Листинг 13: Работа с десятичными знаками и дробями
>>> import fractions
>>> import decimal
>>> a = fractions.Fraction(1,10)
>>> b = fractions.Fraction(decimal.Decimal(0.1))
>>> a,b
(Fraction(1, 10), Fraction(3602879701896397, 36028797018963968))
>>> a == b
False
Заключение
Правильное хранение и обработка значений с плавающей запятой - это непростая задача, требующая большого внимания со стороны программистов. Округление значений может помочь, но обязательно проверьте правильный порядок округления и метод, который вы используете. Это наиболее важно при разработке таких вещей, как финансовое программное обеспечение, поэтому вы захотите проверить правила местного законодательства на предмет округления.
Python предоставляет вам все необходимые инструменты и поставляется с «батареями в комплекте». Удачного взлома!
Благодарности
Автор благодарит Золеку Хофманн за ее критические комментарии при подготовке статьи.