Модули Decimal и Fraction в Python
Из-за ограничений в сохранении точного значения чисел, даже простейшие математические операции могут выдавать ошибочный результат. Эти ограничения легко преодолимы — достаточно использовать десятичный модуль Decimal в Python. А в выполнении расчетов на основе дробей поможет модуль фракций — Fraction.
О принципах работы Decimal и Fraction и пойдет речь в данном обзоре.
Для чего нужен модуль Decimal?
Некоторые пользователи задаются вопросом, зачем нам нужен модуль для выполнения простейшей арифметики с десятичными числами, когда мы вполне можем сделать то же самое с помощью чисел с плавающей точкой 🤷♂️?
Перед тем, как мы ответим на данный вопрос, мы хотим, чтобы вы сами посчитали в Python, какой результат будет в данном примере: 0.1+0.2? Вы будете удивлены, когда узнаете, что правильный ответ — это не 0,3, а 0,30000000000000004.
Чтобы понять, почему в расчетах возникла ошибка, попробуйте представить 1/3 в десятичной форме. Тогда вы заметите, что число на самом деле не заканчивается в базе 10. Так как все числа должны быть каким-то образом представлены, при их сохранении в консоли делается несколько приближений, что и приводит к ошибкам.
Cпециально для читателей-гуманитариев, у нас есть объяснение принципов работы модулей Питона: «Она на долю секунды отвела взгляд» и "Она отвела взгляд на короткое время" — чувствуете разницу?
Чтобы получить точные результаты, подобные тем, к которым мы привыкли при выполнении расчетов вручную, нам нужно что-то, что поддерживает быструю, точно округленную, десятичную арифметику с плавающей запятой, и модуль Decimal отлично справляется с этой задачей. Теперь, когда мы разобрались с теорией, переходим к принципам работы десятичного модуля.
Модуль Decimal
Синтаксис
С помощью Decimal вы можете создавать десятичные числа.
Decimal обеспечивает поддержку правильного округления десятичной арифметики с плавающей точкой.
>>> from decimal import Decimal
>>> number1 = Decimal("0.1")
>>> number2 = Decimal("0.7")
>>> print(number1 + number2)
0.8
Decimal
, в отличие от float
, имеет ряд преимуществ:
- работает так же, как школьная арифметика;
- десятичные числа представлены точно (в отличие от float, где такие числа как 1.1 и 5.12 не имеют точного представления);
- точность десятичного модуля Decimal можно изменять (с помощью
getcontext().prec
);
Контекст
Базовые параметры Decimal можно посмотреть в его контексте, выполнив функцию getcontext()
:
>>> from decimal import getcontext
>>> getcontext()
Context(prec=3, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999,
capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[InvalidOperation,
DivisionByZero, Overflow])
Точность
Контекстом в Decimal можно управлять, устанавливая свои значения. Например, для того, чтобы управлять точностью Decimal, необходимо изменить параметр контекста prec
(от англ. precision — точность):
>>> from decimal import Decimal, getcontext
>>> getcontext().prec = 2
>>> print(Decimal('4.34') / 4)
1.1
>>> getcontext().prec = 3
>>> print(Decimal('4.34') / 4)
1.08
Округление
Округление осуществляется с помощью метода quantize()
. В качестве первого аргумента — объект Decimal, указывающий на формат округления:
>>> from decimal import Decimal
>>> getcontext().prec = 4 # установим точность округление
>>> number = Decimal("2.1234123")
>>> print(number.quantize(Decimal('1.000')))
2.123 # округление до 3 чисел в дробной части
>>> print(number.quantize(Decimal('1.00')))
2.12 # округление до 2 чисел в дробной части
>>> print(number.quantize(Decimal('1.0')))
2.1 # округление до 1 числа в дробной части
💁♀️ Важно: если точность округления установлена в 2
, а формат округления Decimal('1.00')
, возникнет ошибка:
>>> print(number.quantize(Decimal('1.000')))
Traceback (most recent call last):
File "<pyshell#78>", line 1, in <module>
print(number.quantize(Decimal('1.00')))
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]
Чтобы избежать ее, необходимо поменять точность округления, как было сделано в примере выше:
>> getcontext().prec = 4
>>> print(number.quantize(Decimal('1.000')))
2.123
Помимо первого параметра, quantize() принимает в качестве второго параметра стратегию округления:
ROUND_CEILING
— округление в направлении бесконечности (Infinity);ROUND_FLOOR
— округляет в направлении минус бесконечности (- Infinity);ROUND_DOWN
— округление в направлении нуля;ROUND_HALF_EVEN
— округление до ближайшего четного числа. Число 4.9 округлится не до 5, а до 4 (потому что 5 — не четное);ROUND_HALF_DOWN
— округление до ближайшего нуля;ROUND_UP
— округление от нуля;ROUND_05UP
— округление от нуля (если последняя цифра после округления до нуля была бы 0 или 5, в противном случае к нулю).
>>> from decimal import Decimal, ROUND_CEILING
>>> number = Decimal("0.029")
>>> print(number.quantize(Decimal("1.00"), ROUND_CEILING))
0.03
Помните, что как округление, так и точность вступают в игру только во время арифметических операций, а не при создании самих десятичных дробей
Полезные методы Decimal
Итак, вот некоторые полезные методы для работы с десятичными числами в Decimal:
sqrt()
— вычисляет квадратный корень из десятичного числа;exp()
— возвращает e^x (показатель степени) десятичного числа;ln()
— используется для вычисления натурального логарифма десятичного числа;log10()
— используется для вычисления log (основание 10) десятичного числа;as_tuple()
— возвращает десятичное число, содержащее 3 аргумента, знак (0 для +, 1 для -), цифры и значение экспоненты;- fma(a, b) — "fma" означает сложить, умножить и добавить. Данный метод вычисляет
(num * a) + b
из чисел в аргументе. В этой функции округлениеnum * a
не выполняется; copy_sign()
— печатает первый аргумент, копируя знак из второго аргумента.
📜 Полный список методов Decimal описан в официальной документации
Модуль Fraction
Этот модуль пригодится в тех случаях, когда вам необходимо выполнить вычисления с дробями, или когда результат должен быть выражен в формате дроби.
>>> from fractions import Fraction as frac
>>> print(Fraction(33.33))
2345390243441541/70368744177664
>>> print(Fraction('33.33'))
3333/100
Модуль Fraction особенно полезен, потому что он автоматически уменьшает дробь. Выглядит это вот так:
>>> Fraction(153, 272)
Fraction(9, 16)
Кроме того, вы можете выполнять бинарные (двоичные) операции над дробью также просто, как вы используете int
или float
. Просто добавьте две фракции:
>>> Fraction(1, 2) + Fraction(3, 4)
Fraction(5, 4)
Теперь давайте попробуем возвести дробь в степень:
>>> Fraction(1, 8) ** Fraction(1, 2)
0.3535533905932738
Когда использовать Decimal и Fraction?
Потребность в максимальной точности расчетов на практике чаще всего возникает в отраслях и ситуациях, где некорректно выбранная точность расчетов может обернуться серьезными финансовыми потерями:
- Обмен валют. Особенно если этот процесс подразумевает не просто конвертацию евро в рубли, тенге или иную валюту, а выполнение более сложных операций.
- Масштабируемые расчеты. К примеру, на фабрике начинают готовить печенье по бабушкиному рецепту, в котором упоминается "1/3 столовой ложки" определенного ингредиента. Сколько именно литров или миллилитров составит эта треть, если применять ее не к одной порции печенья, а к промышленным масштабам? А сколько это составит в пересчете на "неродную" систему мер, то есть фунтов или унций?
- Работа с иррациональными числами. Если вы планируете запускать спутник или возводить энергетическую станцию, точность расчетов необходимо задать еще до того, как вы приступите к самым первым вычислениям. И оповестить об этом всех, кто имеет хоть какое-то отношение к проекту.
Таким образом, этих двух модулей должно быть достаточно, чтобы помочь вам выполнять общие операции как с десятичными, так и с дробными числами. Как мы уже говорили, вы можете использовать эти модули вместе с математическим модулем для вычисления значения всех видов математических функций в желаемом формате.
Модуль Decimal незаменим, если нужно считать деньги: с его помощью вы сможете подсчитать точную сумму, вплоть до копеек.
Fraction считает просто и честно: любители онлайн-игр приспособили его для подсчетов в игровой математике.
Привет Pytonchik! И снова вынужден обратиться за помощью.
Начал писать практическую работу о
decimal
, мне нужно проверить насколько он медленееfloat
. Я не знаю как это сделать,time
выводит ноль секунд если программа отработала меньше 1 секунды, и это понятно. Проблема возникает с модулемtimeit
. Судя из той теории что я прочёл, модульtimeit
выводит минимальное время выполнения, в свою очередь мне нужно среднее значение, хотя в той же теории было сказано что статистика не так важна и лучше ориентироваться на это самое минимальное значение. Ну хорошо, я начал эксперементировать.import timeit start = timeit.timeit("(0.15 + 0.23 + 0.41)*13") print(start) # 0.15163580002263188
Тут вывело вроде как нормальное значение, насколько я понимаю это 0 секунд и 15 нано секунд. Это минимальное значение при условии что параметр
number
стоит по умолчанию 1000000. Теперь мы такие "умные" и "о какой же простой модуль" что я подумал пока ознакамливался с ним установитьnumber=100
.import timeit start = timeit.timeit("(0.15 + 0.23 + 0.41)*13", number=100) print(start) # 2.269999822601676e-05
И о чудо! Мы просто сократили количество запуска кода до ста, и вследствии чего код стал обрабатываться 2 секунды, что не соответствует действительности, и по логике не должно так работать, мы же просто сократили количество повторений не меняя сам код.
Смотрим что произойдёт с
decimal
.import timeit start = timeit.timeit("(Decimal('0.15') + Decimal('0.23') + Decimal('0.41'))*Decimal('13')", setup="from decimal import Decimal") print(start) # 1.7781018000096083
Ну вот казалось бы из миллиона повторений
float
в разы быстрееdecimal
что мы и доказываем, а теперь мы ограничим опять чтобы таймер замерял 100 попыток.import timeit start = timeit.timeit("(Decimal('0.15') + Decimal('0.23') + Decimal('0.41'))*Decimal('13')", setup="from decimal import Decimal", number=100) print(start) # 0.00021630001720041037
И как мы видим, программа нам говорит что при ста повторений, те 15 нано секунд у
float
и в подмётки не годятся нашемуdecimal
что так же быть не может.Очень надеюсь на помощь, т.к. решение этого вопроса в интернете я не нашёл. Подскажи пожалуйста как лучше замерить время, чтобы оно не выдавало таких странных результатов. Укажи что я не правильно может делаю, и что же всё же лучше, ориентироваться на 2 самых быстрых выполнения кода, или всё же всё это суммировать в цикле и делить на количество повторений, т.к. пример из интернета по типу "берем любой прежний код и делим к примеру на 100" нам не выведет среднее значение, должно вывести просто самое быстрое выполнение делённое на 100. Заранее спасибо за помощь.
p.s.
number
это не повторениеtimeit
, а количество итераций, то есть код из 4 строк приnumber
7 замерит 4 строки и до третий включительно и это будет общее время, но это не отменяет моего непонимания почему мы миллион раз складываем и умножаем в первом примере за 15 нано секунд, а 100 раз за 2 секунды и почему у нас так или иначе когда миллион разdecimal
выполняется, более менее логичное время, а при 100 выполненияхdecimal
в отличии отfloat
убежал вперед планеты всей, когда это должно быть наоборот...p.s.s. Заглянув в вашу теорию по числам, я понял что всё выводится правильно, просто в представлении которое я пока не понимаю, 0.00021630001720041037 это число больше чем 2.269999822601676e-05, т.к. e-05 означает количество нулей. Правда я не понимаю что означает 05, то есть было бы -5 было бы еще 5 нулей, 1 в основной части и 4 в дробной, но 05 не понимаю, как воспринимать. Чтобы мне это понять мне нужно было убить 6 часов на элементарные базовые вещи, и написать длиннющий коментарий, который теперь всё что выше не имеет смысла... Подскажите где можно подробнее почитать с числами j и e, и что эти буквы впринципе значат, т.к. из math это некое похожее число 2,78, тогда зачем оно в этой записи вывода, что оно означает?
Вы все правильно написали в p.s.s.
2.269999822601676e-05
— это экспоненциальная запись числа. Она используется при очень больших или очень малых числах. В данном случае перед нами очень маленькое число0.0000227
.Перевести в Python из экспоненциальной записи в обычную можно так:
print('{:0.7f}'.format(2.269999822601676e-05)) # 0.0000227
Но при тестировании лучше задать побольше итераций, чтобы результаты были в секундах.