Автор оригинала: Frank Hofmann.
Использование компьютера для выполнения довольно сложной математики-одна из причин, по которой эта машина была первоначально разработана. До тех пор, пока целые числа и сложения, вычитания и умножения участвуют исключительно в вычислениях, все в порядке. Как только в игру вступают числа с плавающей запятой или дроби, а также деления, это чрезвычайно усложняет все дело.
Как обычный пользователь, мы не полностью осведомлены об этих проблемах, которые происходят за кулисами и могут привести к довольно неожиданным и, возможно, неточным результатам для наших расчетов. Как разработчики, мы должны обеспечить, чтобы соответствующие меры были приняты во внимание, чтобы проинструктировать компьютер работать в правильном направлении.
В нашей повседневной жизни мы используем десятичную систему счисления, основанную на числе 10. Компьютер использует двоичную систему, которая является базой 2, и внутренне он хранит и обрабатывает значения в виде последовательности 1 и 0. Ценности, с которыми мы работаем, должны постоянно трансформироваться между этими двумя представлениями. Как описано в документации 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 decimal и fraction (см. Примеры ниже). Но сначала давайте подробнее рассмотрим термин “округление”.
Что такое Округление?
В нескольких словах процесс округления означает:
…замена [значения] другим числом, которое приблизительно равно исходному, но имеет более короткое, простое или более явное представление.
Источник: Источник:
По сути, он добавляет неточность к точно рассчитанному значению, сокращая его. В большинстве случаев это делается путем удаления цифр после десятичной точки, например, от 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 |
Методы округления
Математики разработали множество различных методов округления, чтобы решить проблему округления. Это включает в себя простое усечение, округление вверх, округление вниз, округление наполовину вверх, округление наполовину вниз, а также округление наполовину от нуля и округление наполовину до четного.
Например, округление наполовину от нуля применяется Европейской комиссией по экономическим и финансовым вопросам при конвертации валют в евро. Некоторые страны, такие как Швеция, Нидерланды, Новая Зеландия и Южная Африка, следуют правилу, называемому “округление наличных денег”, “округление пенни” или “шведское округление”.
[Округление денежных средств] происходит, когда минимальная единица счета меньше самого низкого физического номинала валюты. Сумма, подлежащая оплате по кассовой операции, округляется до ближайшего кратного минимальной доступной денежной единицы, в то время как операции, оплаченные другими способами, не округляются.
Источник: Источник:
В Южной Африке с 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: Вычисления с плавающей запятой с помощью математика модуль
>>> 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: Создание десятичных чисел с помощью десятичный модуль
>>> import decimal >>> decimal.getcontext().prec = 8 >>> a = decimal.Decimal(1) >>> b = decimal.Decimal(7) >>> a / b Decimal('0.14285714')
Теперь сравнение значений float становится намного проще и приводит к тому результату, который мы искали.
Листинг 10: Сравнение с использованием десятичный модуль
>>> 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: Округление значения с помощью квантование()
>>> d = decimal.Decimal(4.6187) >>> d.quantize(decimal.Decimal("1.00")) Decimal('4.62')
И последнее, но не менее важное: мы рассмотрим модуль фракции
. Этот модуль позволяет обрабатывать значения с плавающей запятой в виде дробей, например 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 дает вам все необходимые инструменты и поставляется с “включенными батарейками”. Счастливого взлома!
Признание
Автор хотел бы поблагодарить Золеку Хофманн за ее критические замечания при подготовке этой статьи.