Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs
На данный момент
мы с вами знакомы с тремя основными числовыми типами языка Python:
- int – для целых
чисел;
- float – для
вещественных чисел;
- complex
– для
комплексных чисел.
Причем,
комплексные числа состоят из данных вещественного типа float. Поэтому можно выделить
два основных числовых типа: int и float.
Целочисленные
значения относительно просто кодируются на уровне бит без искажений. Например:
192
= 1100 0000
87
= 0101 0111
5 = 0000 0101
И так далее. А
вот с вещественными числами дела обстоят куда сложнее. Они кодируются в виде конечных
двоичных дробей. Однако не каждое дробное число можно точно представить такими
двоичными дробями. Классический пример – число 0.1, которое не может быть точно
описано на уровне бит стандартной схемой. В итоге получаем следующий известный
арифметический казус в программировании:
0.1+0.1+0.1 = 0.30000000000000004
Из-за неточности
представления числа 0.1, приведенная сумма дает некоторую погрешность. И это
лишь один из многочисленных случаев, когда вещественные операции приводят к
некоторым расхождениям с точными теоретическими значениями.
Хорошая новость
в том, что в большинстве приложений такой незначительной погрешностью можно
пренебречь. Чаще всего так и делают, но при этом имеют в виду ее наличие.
Например, в программировании крайне не рекомендуется выполнять сравнение на
равенство вещественных чисел:
d = float(input())
if d == 0.78: # так делать не надо!
print("Wow!!!")
Из-за
погрешностей представления вещественных чисел такое сравнение может не
сработать, даже при формальном математическом равенстве. Поэтому в таких
ситуациях принято задавать некоторую погрешность при сравнении на равенство
вещественных величин, например, так:
delta = 1e-6
if abs(d-0.78) < delta:
print("Wow!!!")
Однако иногда,
все же, возникают задачи, где нужна математическая точность представления
вещественных чисел и операций с ними. Классический пример – финансовые
программные продукты. Здесь каждая доля копейки имеет значение. И связано это,
отнюдь, не столько из-за чрезмерной жадности финансовых воротил, сколько из-за
уязвимостей, которые несут в себе погрешности вычислений. Чтобы у вас не
осталось в этом никаких сомнений приведу один реальный пример. Группа
программистов, разрабатывая программный модуль по проведению финансовых
транзакций, воодушевилась внезапной гениальной мыслью: а что если каждую
крупную транзакцию округлять в свою пользу так, чтобы получать с нее доли
копейки. Этого же даже никто и не заметит? Сказано, сделано! Прошли годы. Схема
успешно приносила горе-дельцам ощутимую прибыль. Но аудит не дремал. Была
выявлена большая недостача. Тщательное расследование в конце-концов вышло на
эту злополучную схему, виновные были наказаны, справедливость восторжествовала.
Мораль. Даже незначительные искажения данных могут приводить к значимым
финансовым потерям.
Создание объектов класса Decimal
Итак, я думаю,
вы теперь в полной мере осознаете, как иногда важны предельно точные вычисления
с вещественными числами. Поэтому для их проведения в Python был добавлен
специальный класс Decimal.
С помощью этого
класса можно создавать новое представление вещественных чисел, отличное от
стандартного float. Делается это
следующим образом:
a = Decimal('0.1')
b = Decimal(7)
c = Decimal('-0.3')
d = Decimal(-13)
e = Decimal('1e-8')
Обратите
внимание, что вещественные числа следует прописывать в виде строки, чтобы не
было искажений в корректности их точного представления. Формально можно указать
и любое вещественное число:
или с помощью
метода from_float():
g = Decimal.from_float(0.3)
но делать так
крайне не рекомендуется, т.к. сначала 0.1 и 0.3 будут закодированы, как тип float, т.е. неточно,
и только потом искаженные значения разместятся в объектах класса Decimal. Мы в этом
легко можем убедиться:
for x in a, b, c, d, e, f, g:
print(f"{x:.20f}")
В консоли
увидим:
0.10000000000000000000
7.00000000000000000000
-0.30000000000000000000
-13.00000000000000000000
0.00000001000000000000
0.10000000000000000555
0.29999999999999998890
Как видите,
последние значения вместо 0.1 и 0.3 имеют некоторый шум представления. Тогда
как аналогичное число 0.1, записанное в виде строки, такой погрешности не
имеет.
Обратное конвертирование объектов класса Decimal в числа
Объекты класса Decimal всегда можно
конвертировать в стандартные числовые типы данных int или float. Например,
пусть имеется объект:
Чтобы его
преобразовать в тип float достаточно прописать:
res_f = float(res) # 1.57843
Или же его можно
привести к целочисленному типу int:
В этом случае
будет отброшена дробная часть.
Арифметические операции с объектами класса Decimal
С объектами
класса Decimal можно выполнять
все стандартные арифметические операции. Например, пусть заданы числа:
a = Decimal('5.3')
b = Decimal('3.1415')
d = Decimal('7')
e = Decimal('3')
Тогда с ними
можно выполнять следующие вычисления:
a + b # Decimal('8.4415')
b - a # Decimal('-2.1585')
a * e # Decimal('15.9')
b / a # Decimal('0.5927358490566037735849056604')
a // b # Decimal('1')
d % e # Decimal('1')
И с комбинацией
обычных целых чисел языка Python:
e ** 2 # Decimal('9')
a + 7 # Decimal('12.3')
8 / a # Decimal('1.509433962264150943396226415')
d % 4 # Decimal('3')
И так далее. А
вот применять обычные вещественные числа недопустимо, возникнет ошибка TypeError:
Если все же возникает
необходимость использования чисел типа float, то их
предварительно нужно обернуть в класс Decimal:
a + Decimal('0.1') # Decimal('5.4')
При этом
желательно, чтобы вещественное число было представлено в виде строки, иначе
потеряется смысл использования класса Decimal.
Также следует
иметь в виду, что операция деления на ноль:
d / 0 # decimal.DivisionByZero
приводит к
ожидаемому исключению типа decimal.DivisionByZero.
Также с
объектами класса Decimal допустимо выполнять операции
вида:
a += b # Decimal('8.4415')
d *= 2 # Decimal('14')
b /= e # Decimal('1.047166666666666666666666667')
И так далее.
Операции сравнений с объектами класса Decimal
Помимо базовых
арифметических операций с объектами класса Decimal можно
использовать все стандартные операции сравнения. Например:
a == b # False
a == 5.3 # False
a == Decimal('5.3') # True
b < a # True
a >= b # True
d == 7 # True
d == Decimal('7') # True
d != e # True
Смотрите,
сравнение a == 5.3 возвращает
False, тогда как a == Decimal('5.3') –
значение True. Ранее мы уже
говорили, что вещественные числа типа float не следует
сравнивать между собой обычным равенством. Но числа Decimal можно, т.к. они
имеют точное представление в памяти устройства. При сравнении с целыми числами
таких проблем не возникает, поэтому обе команды:
d == 7 # True
d == Decimal('7') # True
возвращают True.
Способ кодирования объектов класса Decimal
На практике
вполне может возникнуть необходимость сохранения значений класса Decimal, например, в БД
или даже экспорта этих значений в другие приложения, где этот класс
отсутствует. Один из самых простых способов это сделать – представить
вещественное число в виде следующего кортежа:
(знак числа,
(кортеж из цифр), значение экспоненты)
Например, для
числа:
путем вызова
метода as_tuple() получим
следующий кортеж:
a.as_tuple() # DecimalTuple(sign=0, digits=(5, 3), exponent=-1)
Здесь sign=0 – означает
положительное число (1 – для отрицательных); digits=(5, 3) – набор
цифр числа; exponent=-1 означает 10^-1
степени, т.е. число из цифр 53 нужно разделить на 10 для получения итогового
значения 5.3.
Давайте то же
самое проделаем с числом:
b = Decimal('-85.12306946')
получим:
DecimalTuple(sign=1,
digits=(8, 5, 1, 2, 3, 0, 6, 9, 4, 6), exponent=-8)
Теперь знак
отрицательный (sign=1), а набор чисел нужно разделить на 10^8 степени,
т.е. последние 8 цифр расположить в дробной части.
Мало того, мы
можем формировать новые числа Decimal на основе такого представления.
Например:
c = Decimal((0, (3, 1, 4, 1, 5), -4)) # Decimal('3.1415')
d = Decimal((1, (8, 7), 0)) # Decimal('-87')
e = Decimal((0, (1, 2), 3)) # Decimal('1.2E+4')
Это удобный
способ точного описания вещественных чисел при их экспорте и импорте.
Специальные значения чисел класса Decimal
Помимо обычных
числовых значений объекты класса Decimal могут принимать еще два
специальных:
- 'NaN'
(либо '-NaN') – Not a Number, не число;
- 'Infinity' (либо '-Infinity')
– бесконечность.
Эти значения
можно задать напрямую следующим образом:
d_nan1 = Decimal('NaN')
d_nan2 = Decimal('-NaN')
и
d_inf1 = Decimal('Infinity')
d_inf2 = Decimal('-Infinity')
Они крайне редко
возникают на практике, но знать о них нужно. Особенность их в том, что,
например, любые арифметические операции с ними приводят к ним же самим:
d_inf1 + a # Decimal('Infinity')
d_nan1 - 5 # Decimal('NaN')
На следующем
занятии мы продолжим эту тему и рассмотрим различные методы и математические
функции для работы с числами класса Decimal.
Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs