decimal - вычисления с заданной точностью

Модуль decimal позволяет устранить недостатки, которые иногда возникают при работе с встроенными числами типа float, а именно:

Начнем с того, что некоторые числа типа float не могут быть точно представлены в двоичной системе счисления, например:

>>> 3.3 + 4.1    #  вместо 7.4 получаем:
7.3999999999999995

Однако, числа типа Decimal всегда ведут себя как истинные десятичные дроби:

>>> from decimal import *
>>> 
>>> Decimal('3.3') + Decimal('4.1')
Decimal('7.4')

Конечно, такая запись простого выражения кажется неоправданно громоздкой. Но в некоторых ситауциях такая "громоздкость" – это мелочь в сравнении с проблемами, которые могут возникнуть в отсутствии необходимой точности.

Очень часто с числами типа float проблематично выполнять операции сравнения:

>>> 3.3 + 4.1 == 7.4   #  должно быть True, но...
False
>>> 
>>> 
>>> #  Но с числами типа Decimal все верно:
... Decimal('3.3') + Decimal('4.1') == Decimal('7.4')
True

Поэтому использование лишь чисел типа float очень затрудняет разработку финансовых приложений.

Еще, ведя денежные расчеты, бывает необходимо отображать значимые разряды, например 1.500 + 1.500 должно быть равно 3.000, но:

>>> 1.500 + 1.500
3.0
>>> 
>>> Decimal('1.500') + Decimal('1.500')
Decimal('3.000')

Числа типа float имеют фиксированную точность, в то время как числа типа Decimal настраиваемую:

>>> 7**0.5    #  у float всегда одна и таже точность
2.6457513110645907
>>> 
>>> #  Модуль decimal позволяет настраивать точность:
... getcontext().prec = 100
>>> 
>>> #  смотрим результат:
... Decimal(7).sqrt()
Decimal('2.645751311064590590501615753639260425710259183082450180368334459201068823230283627760392886474543611')

Кроме того модуль decimal позволяет полность контролировать все вычисления и их точность, т.е. не просто задавать нужное количество цифр после запятой, а именно перехватывать ситуации в которых эта точность нарушается. Что позволяет не просто блокировать неточные операции, но и строить приложения, которые полностью соответствуют Общей спецификации десятичной арифметики.


Устройство модуля decimal

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

Десятичное число в данном модуле относится к немутирующему (неизменяемому) типу данных, т.е. как и все основные числа в Python они не могут быть изменены напрямую. Десятичное число может обладать знаком, состоять из мантисы и экспоненты. Для сохранения значимости конечные нули не усекаются. Такие специальные значения, как -inf, inf и nan так относятся к десятичным числам. В данном модуле значения \(-0\) и \(+0\) считаются различными.

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

Сигналы – это специальные условия, которые возникают в процессе вычислений. Опционально, данные условия могут рассматриваться, как исключительные (вызывать ошибку) или информационные, а могут и просто игнорироваться.

Каждый сигнал обладает отдельным флагом и активатором его перехвата. При возникновении сигнала, его флаг устанавливается в единицу и если активатор его перехвата так же установлен в единицу, то вызывается исключение. Следует помнить, что значения флагов не сбрасываются после обработки исключений, т.е. каждый раз их значения необходимо сбрасывать.


Как использовать модуль?

Все начинается с импорта модуля:

>>> from decimal import *

Я думаю, что вы уже заметили какими громоздкими становятся выражения при использовании данного модуля, поэтому данный способ импорта, является наиболее удобным (хотя и не обязательным).

Следующим шагом (опять же необязательным) может быть просмотр текущего контекста с помощью метода getcontext():

>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN,
        Emin=-999999, Emax=999999, capitals=1,
        clamp=0, flags=[], traps=[InvalidOperation,
        DivisionByZero, Overflow])

Ну и чаще всего нас интересует параметр prec (от англ. precision – точность), т.е. текущая точность равна 28 знакам после запятой. Давайте сделаем 15 знаков:

>>> getcontext().prec = 15

Теперь можем переходить к созданию чисел типа Decimal, а создаются они с помощью одноименного метода Decimal(), которому можно передать, например, число типа int:

>>> Decimal(0)
Decimal('0')
>>> 
>>> Decimal(12)
Decimal('12')
>>> 
>>> Decimal(5**5)
Decimal('3125')

Если же передать число типа float, то следует иметь в виду, что число типа Decimal будет создано, не из того числа, которое вы видите, а из того числа, которое хранится в компьютере. Попытаемся сделать число Decimal из постоянной Планка:

>>> getcontext().prec = 25
>>> 
>>> Decimal(1.054572)    #  ожидаем получить число Decimal('1.054572'), но...
Decimal('1.0545720000000000649009734843275509774684906005859375')

Появившийся "хвост" числа как раз и демонстрирует нам то, что с ним происходит после погружения в двоичный мир компьютера и обратного преобразования в десятичную систему счисления. А именно то что конечная десятичная дробь в двоичной системе счисления может быть бесконечной, что приводит к неизбежному округлению и следовательно к потере точности:

>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal(0.2)
Decimal('0.200000000000000011102230246251565404236316680908203125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')

Подобного поведения можно избежать если передать Decimal() вместо числа, его строковое представление:

>>> Decimal('1.054572')    #  строковое представление числа типа float
Decimal('1.054572')
>>> 
>>> Decimal('112')    #  строковое представление числа типа int
Decimal('112')

Можно использовать встроенную функцию str():

Decimal(str(3**0.5 / 2))
Decimal('0.8660254037844386')

А можем и не пользоваться, так как числа типа Decimal поддерживают все математические операции:

>>> Decimal(3) ** Decimal('0.5') / Decimal(2)
Decimal('0.866025403784438646763723')

В математических выражениях мы можем комбинировать числа типа int с числами типа Decimal:

>>> 3 ** Decimal('0.5') / 2
Decimal('0.866025403784438646763723')

Есть еще один любопытный способ создания – передача числа в виде кортежа:

>>> Decimal((0, (1, 2, 3, 4, 5, 6), -5))
Decimal('1.23456')
>>> 
>>> Decimal((1, (1, 2, 3, 4, 5, 6), 5))
Decimal('-1.23456E+10')

Кортеж должен иметь следующий вид (sign, (digit, digit, ..., digit), m), где sign – это знак числа: \(0\) – число положительное, \(1\) – отрицательное; digit – это любая десятичная цифра; m – это мантисса.

Значение prec не изменяет количество значимых цифр вводимого числа, но обязательно изменит его после выполнения математических операций:

>>> getcontext().prec = 5
>>> 
>>> Decimal('1.23456789')
Decimal('1.23456789')
>>> 
>>> Decimal('1.23456789') - Decimal('0.23456789')
Decimal('1.0000')
>>> 
>>> Decimal('1.23456789') + Decimal('0.76543211')
Decimal('2.0000')

Надо сказать, что по началу модуль decimal реально кажется громоздким и неуклюжим, но благодаря тому, что он прекрасно поддерживает большинство встроенных функций, это ощущение довольно быстро пропадает. Ниже приведены несколько примеров, которые позволяют в этом убедиться.

Как-то так можно вычислить основание натурального логарифма:

>>> getcontext().prec = 50
>>> 
>>> E = [(1 + 1 / Decimal(n)) ** Decimal(n) for n in range(100, 1001, 100)]
>>> 
>>> for e_i in E: print(e_i)
... 
2.7048138294215260932671947108075308336779383827810
2.7115171229293747985489939701629041265265189286441
2.7137651579427212324492280834060860037481025686644
2.7148917443812869193244970926575260752591601832894
2.7155685206517259295998493080571813310041784543130
2.7160200488805888626827844198198102515693104054477
2.7163427377297350409498498101967759364269307270928
2.7165848466825289049754373476067482569932619617004
2.7167732083805291029889071779043927664967736562898
2.7169239322358924573830881219475771889643150188366

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

>>> getcontext().prec = 5
>>> from random import *
>>> 
>>> data = [1*Decimal(random()) for i in range(10)]
>>> data
[Decimal('0.66604'), Decimal('0.81747'), Decimal('0.96528'), Decimal('0.58758'), Decimal('0.082032'), Decimal('0.57953'), Decimal('0.30142'), Decimal('0.49962'), Decimal('0.91941'), Decimal('0.61694')]

Обратите внимание на команду 1*Decimal(random()), по сути мы добавили умножение на \(1\) только для того что бы сработало ограничение getcontext().prec = 5, которое как мы помним срабатывает только после выполнения математических операций. Если попробовать обойтись без него, то получится что-то вроде:

>>> Decimal(random())
Decimal('0.0639194079415068561189627871499396860599517822265625')
>>> 
>>> Decimal(str(random()))
Decimal('0.2678159869052429')

В общем, умножение на \(1\) (прибавление \(0\)) распространенная практика. Что ж продолжим, мы создали список data, дальше мы можем проделывать с ним все, к чему привыкли:

>>> sum(data) / len(data)    #  среднее арифметическое
Decimal('0.60352')
>>> 
>>> min(data)    #  минимальный элемент
Decimal('0.082032')
>>> 
>>> max(data)    #  максимальный элемент
Decimal('0.96528')
>>> 
>>> sorted(data)    #  сортировка списка
[Decimal('0.082032'), Decimal('0.30142'), Decimal('0.49962'), Decimal('0.57953'), Decimal('0.58758'), Decimal('0.61694'), Decimal('0.66604'), Decimal('0.81747'), Decimal('0.91941'), Decimal('0.96528')]
>>> 
>>> 
>>> a, b, c, d = data[3:7]
>>> 
>>> a, b, c, d = data[3:7]    #  распаковка среза
>>> 
>>> str(a), int(10*b), float(c), complex(d)    #  приведение к другому типу данных
('0.58758', 0, 0.57953, (0.30142+0j))
>>> 
>>> a**c + b**d    #  выполнение математических действий
Decimal('1.2054')

Некоторые математические функции реализованы как методы класса Decimal:

>>> getcontext().prec = 55
>>> 
>>> Decimal(3).sqrt()
Decimal('1.732050807568877293527446341505872366942805253810380628')
>>> Decimal('2.718281').ln()
Decimal('0.9999996952269029620733811292599947317309197069580222983')
>>> Decimal(1).exp()
Decimal('2.718281828459045235360287471352662497757247093699959575')
>>> Decimal(1001).log10()
Decimal('3.000434077479318640668921387777988866020003775177486773')

Округлить число до необходимого количества значащих цифр можно с помощью метода quantize():

>>> Decimal('1.515').quantize(Decimal('1.11'), rounding = ROUND_DOWN)
Decimal('1.51')
>>> Decimal('1.515').quantize(Decimal('1.11'), rounding = ROUND_UP)
Decimal('1.52')

Всего рассмотренного выше, в принципе, уже достаточно для создания простых и даже финансовых приложений. Но для более сложных вычислений может потребоваться контекст, который должен отвечать специфике задачи. Мы уже видели, что обратиться к контексту и зменять его параметры можно с помощью функции getcontext(). Создать собственный контекст вычислений можно с помощью конструктора Context(), а сделать его активным позволяет функция setcontext():

>>> my_context = Context(prec = 3, rounding = ROUND_DOWN)
>>> setcontext(my_context)
>>> 
>>> Decimal(1)/Decimal(8)
Decimal('0.125')
>>> Decimal(1)/Decimal(16)
Decimal('0.0625')
>>> Decimal(1)/Decimal(17)
Decimal('0.0588')
>>> Decimal(180)/Decimal(17)
Decimal('10.5')

Так же в соответствии со стандартом модуль предоставляет два готовых контекста BasicContext и ExtendedContext:

>>> BasicContext
Context(prec=9, rounding=ROUND_HALF_UP, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[Clamped, InvalidOperation, DivisionByZero, Overflow, Underflow])
>>> 
>>> ExtendedContext
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[])

Если приглядеться к BasicContext, то мы увидим что у него установлено гораздо больше сигналов ошибок, что может быть полезно при отладке приложений:

>>> setcontext(BasicContext)
>>> 
>>> Decimal(1)/Decimal(0)    #  Вызовет ошибку
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.DivisionByZero: [<class 'decimal.DivisionByZero'>]
>>> 
>>> 
>>> setcontext(ExtendedContext)
>>> 
>>> Decimal(1)/Decimal(0)    #  А теперь это не считается ошибкой
Decimal('Infinity')

Для мониторинга ошибок или других интересующих нюансов вычислений контексты имеют сигналы, которые отслеживаются с помощью флагов. Данные флаги остаются активными до явного сброса, поэтому для повторного отслеживания интересующих сигналов их нужно явно сбросить. Посмотрите на параметр flags до и после вычислений:

>>> setcontext(ExtendedContext)
>>> getcontext()
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[])
>>> 
>>> Decimal(1)/Decimal(3)
Decimal('0.333333333')
>>> 
>>> getcontext()
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[])
>>> 
>>> Decimal(1)/Decimal(0)
Decimal('Infinity')
>>> 
>>> getcontext()
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[DivisionByZero, Inexact, Rounded], traps=[])

В конечном итоге в списке флагов мы видим, что выполнялось деление на \(0\), что результат был округлен (т.е. цифры за пределами prec были отброшены) и что результат является не точным (т.к. отброшенные цифры не были нулевыми). Как видите информация довольно полезная, но ее нельзя снова отследить пока флаги не сброшены. А сбросить их позволяет метод clear_flags():

>>> getcontext().clear_flags()
>>> getcontext()
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[])

Активировать отдельные ловушки можно с помощью параметра traps:

>>> getcontext().traps[Rounded] = 1
>>> getcontext()
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[Rounded])
>>> 
>>> Decimal(1)/Decimal(16)
Decimal('0.0625')
>>> 
>>> Decimal(1)/Decimal(17)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.Rounded: [<class 'decimal.Rounded'>]

После установки флага Rounded в активное значение команда Decimal(1)/Decimal(16) выполнилась без ошибки, потому что результат операции входит в диапазон точности prec и следовательно не нуждается в округлении. А вот результат команды Decimal(1)/Decimal(17) должен быть округлен, следовательно, а значит вызывать ошибку.

Таким образом мы можем перехватывать множество сигналов, что позволяет создавать очень надежные приложения.


Объект Decimal

class decimal.Decimal(value="0", context=None)
Создает число (объект) типа Decimal из указанного значения в параметре value.
>>> Decimal()
Decimal('0')
>>> 
>>> Decimal(1)
Decimal('1')
>>> 
>>> Decimal('1.75')
Decimal('1.75')

По умолчанию value = 0, но это может быть число типа int, float, кортеж или другой объект типа Decimal. Так же это значение может быть строкой, но ее содержание должно соответствовать синтаксису записи чисел типа Decimal. Допустимыми подстроками являются символы знака: '-' и '+'; символы цифр: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'; строки специальных значений: 'Infinity', 'Inf', 'NaN', 'Nan', 'sNaN', 'sNan'. А сами строки могут иметь следующий вид:

>>> #  Целочисленное представление:
... Decimal('1'), Decimal('12045')
(Decimal('1'), Decimal('12045'))
>>> 
>>> #  Десятичные дроби:
... Decimal('0.1'), Decimal('.1')
(Decimal('0.1'), Decimal('0.1'))
>>> 
>>> #  Экспоненциальную форму:
... Decimal('1.1e2'), Decimal('-1.1E2'), Decimal('e3')
(Decimal('1.1E+2'), Decimal('-1.1E+2'), Decimal('NaN'))
>>> 
>>> #  Не число:
... Decimal('NaN'), Decimal('Nan'), Decimal('Nan231')
(Decimal('NaN'), Decimal('NaN'), Decimal('NaN231'))
>>> 
>>> Decimal('sNaN'), Decimal('sNan'), Decimal('sNan231')
(Decimal('sNaN'), Decimal('sNaN'), Decimal('sNaN231'))
>>> 
>>> Decimal('-NaN'), Decimal('-sNaN')
(Decimal('-NaN'), Decimal('-sNaN'))
>>> 
>>> #  Бесконечности:
... Decimal('Infinity'), Decimal('-Infinity'), Decimal('+Infinity')
(Decimal('Infinity'), Decimal('-Infinity'), Decimal('Infinity'))
>>> 
>>> Decimal('Inf'), Decimal('-Inf'), Decimal('+Inf')
(Decimal('Infinity'), Decimal('-Infinity'), Decimal('Infinity'))

Помимо прочего строками могут быть десятичные цифры в виде строк Юникода, в том числе и других алфавитов:

>>> Decimal('\uff19')
Decimal('9')
>>> 
>>> Decimal('-\uff12\uff10')
Decimal('-20')
>>> 
>>> Decimal('-\uff12\uff10E\uff13')
Decimal('-2.0E+4')

Числа типа float переводятся в числа типа Decimal без потерь, т.е. именно так как они хранятся в памяти компьютера:

>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

Только если значение сигнала FloatOperation установлено в единицу то преобразование приведет к ошибке (по умолчанию сигнал отключен):

>>> getcontext().traps[FloatOperation] = 1
>>> 
>>> Decimal(0.1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.FloatOperation: [<class 'decimal.FloatOperation'>]

Аргумент context позволяет задать поведение для тех случаев когда строка является искаженной. По умолчанию, этот параметр установлен в значение None, что приводит любую невалидную строку в значение NaN:

>>> Decimal('dssd')
Decimal('NaN')

В случае если параметр context перехватывает сигнал InvalidOperation, то вместо значения NaN появляется сообщение об ошибке:

>>> getcontext().traps[InvalidOperation] = 0
>>> Decimal('1.1a')
Decimal('NaN')
>>> 
>>> getcontext().traps[InvalidOperation] = 1
>>> Decimal('1.1a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]

Ну а значениями данного параметра могут быть только контексты вычислений:

>>> Decimal('1.1a', context = ExtendedContext)
Decimal('NaN')
>>> 
>>> Decimal('1.1a', context = BasicContext)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]

Числа типа Decimal являются немутирующими (неизменяемыми) объектами, т.е. могут быть ключами в словарях:

>>> data = {}
>>> 
>>> data[Decimal('1.1')] = 'Anna'
>>> data[Decimal('1.2')] = 'Sam'
>>> 
>>> data
{Decimal('1.1'): 'Anna', Decimal('1.2'): 'Sam'}

Числа типа Decimal во многом могут использоваться точно так же как и числа типа int и float. Есть лишь несколько незначительных отличий. При выполнении оператора '%' (остаток от деления) к числам типа Decimal знак результата будет таким же как у делимого, а не делителя:

>>> -10 % 4, 10 % -4
(2, -2)
>>> 
>>> Decimal(-10) % Decimal(4)
Decimal('-2')
>>> 
>>> Decimal(10) % Decimal(-4)
Decimal('2')

Из-за этого иначе себя ведет и оператор целочисленного деления '//', который так же как и для чисел int и float работает так что бы удовлетворять условию x == (x // y) * y + x % y:

>>> -10 // 4
-3
>>> 
>>> Decimal(-10) // Decimal(4)
Decimal('-2')

Это различие объясняется тем что числа типа Decimal выполняют требования общей спецификации десятичной арифметики, в то время как встроенные типы int и float реализованы на языке C и соответственно выполняют требования которые предъявляются к данному языку.

Числа типа Decimal и float, а так же рациональные дроби fractions.Fraction, как правило, не могут присутствовать вместе в одном математическом выражении. Однако, операции сравнения могут выполняться между числом Decimal и другим числом любого типа (если только не установлен сигнал FloatOperation):

>>> Decimal('1.21') > 1.2
True
>>> Decimal('1.21') == 1.2     #  из-за особенностей float получим False
False
>>> 
>>> 
>>> from fractions import Fraction
>>> 
>>> Decimal('1.2') == Fraction(6, 5)
True

Методы объекта Decimal

Объекты типа Decimal обладают множеством специализированных методов.

adjusted()
Возвращает показателя степени числа, при котором слева от десятичной точки остается только одна цифра. Полезен для определения самой значимой цифры числа.
>>> from decimal import *
>>> 
>>> Decimal('0.0231').adjusted()
-2
>>> 
>>> Decimal('2.31').adjusted()
0
>>> 
>>> Decimal('231').adjusted()
2
as_tuple()
Возвращает число в виде именованного кортежа DecimalTuple(sign, digits, exponent).
>>> from decimal import *
>>> 
>>> num = Decimal('0.0231').as_tuple()
>>> num
DecimalTuple(sign=0, digits=(2, 3, 1), exponent=-4)
>>> 
>>> #  Кортеж является именованным:
... num[1]    #  обращение к элементу по его индексу
(2, 3, 1)
>>> num.digits     #  обращение к элементу по его имени
(2, 3, 1)
canonical()
Возвращает число в канонической форме. Учитывая, что Decimal() и так возвращает число в канонической форме, то применение данного метода не приведет к каим-то инным изменениям аргумента.
>>> from decimal import *
>>> 
>>> Decimal('0.0231')
Decimal('0.0231')
>>> 
>>> Decimal('0.0231').canonical()
Decimal('0.0231')
compare(other, context=None)
Команда Decimal(a).compare(Decimal(other = b)) позволяет выполнить сравнение двух чисел и возвращает Decimal('-1') если a < b, Decimal('0') если a == b и Decimal('1') если a > b. Если a или b равно Decimal('NaN'), то будет возвращено Decimal('NaN').
>>> from decimal import *
>>> 
>>> Decimal('0.0231').compare(Decimal('0.0232'))
Decimal('-1')
>>> 
>>> Decimal('0.0231').compare(Decimal('NaN'))
Decimal('NaN')

Параметр context может принимать объект любого контекста вычислений.

compare_signal(other, context=None)
Метод абсолютно аналогичный методу compare() лишь с тем отличием, что имеет установленные сигналы, перехватывающие сравнение числа с значением NaN или если оба сравниваемых значения NaN (равно как и для sNaN) и вызывающий исключение InvalidOperation.
>>> from decimal import *
>>> 
>>> Decimal('0.0231').compare_signal(Decimal('NaN'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module<
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'<]
>>> 
>>> Decimal('NaN').compare_signal(Decimal('NaN'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module<
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'<]
>>> 
>>> Decimal('0.0231').compare_signal(Decimal('0.1'))
Decimal('-1')
>>> 
>>> Decimal('0.0231').compare_signal(Decimal('0.001'))
Decimal('1')

Параметр context может принимать объект любого контекста вычислений.

compare_total(other, context=None)
Аналогичен методу compare() но позволяет выполнять сравнение не на основе значения чисел, а на основе их абстрактного представления (см. Общую спецификацию десятичной арифметики).
>>> from decimal import *
>>> 
>>> Decimal('1').compare_total(Decimal('1.0'))
Decimal('1')
>>> Decimal('1.0').compare_total(Decimal('1'))
Decimal('-1')
>>> 
>>> Decimal('1.0').compare_total(Decimal('NaN'))
Decimal('-1')
>>> Decimal('NaN').compare_total(Decimal('NaN'))
Decimal('0')

Эта операция не зависит от контекста, т.к. значения NaN включены в общий порядок сравнения. Но если второй операнд не может быть точно представлен то Python реализованный на языке C может вызвать исключение InvalidOperation.

compare_total_mag(other, context=None)
Метод аналогичен compare_total(), но игнорирует знак операндов и по сути аналогичен команде Decimal(a).copy_abs().compare_total(Decimal(b).copy_abs()).
>>> from decimal import *
>>> 
>>> Decimal('1.0').compare_total(Decimal('-1'))
Decimal('1')
>>> 
>>> Decimal('1.0').compare_total_mag(Decimal('-1'))
Decimal('-1')

Эта операция не зависит от контекста, т.к. значения NaN включены в общий порядок сравнения. Но если второй операнд не может быть точно представлен то Python реализованный на языке C может вызвать исключение InvalidOperation.

conjugate()
Возвращает исходный объект без каких-либо изменений (как есть). Данный метод реализован только для соответствия спецификации.
copy_abs()
Возвращает абсолютное значение числа. Данная операция не зависит от контекста, не изменяет флаги, не выполняет округления.
>>> from decimal import *
>>> 
>>> Decimal('-0.1200').copy_abs()
Decimal('0.1200')
copy_negate()
Возвращает число с измененным знаком (схожа с унарным оператором '-'). Данная операция не зависит от контекста, не изменяет флаги, не выполняет округления.
>>> from decimal import *
>>> 
>>> Decimal('-0.1200').copy_negate()
Decimal('0.1200')
>>> 
>>> Decimal('0.1200').copy_negate()
Decimal('-0.1200')
copy_sign(other, context=None)
Возвращает число с тем же знаком, что и у числа other.
>>> from decimal import *
>>> 
>>> Decimal('-0.1200').copy_sign(Decimal('1.1'))
Decimal('0.1200')
>>> 
>>> Decimal('0.1200').copy_sign(Decimal('-1.1'))
Decimal('-0.1200')
>>> 
>>> Decimal('-0.1200').copy_sign(Decimal('-1.1'))
Decimal('-0.1200')
exp(context=None)
Возвращает экспоненту указанного числа x (e**x). Округление результата происходит в режиме ROUND_HALF_EVEN
>>> from decimal import *
>>> 
>>> Decimal(0).exp()
Decimal('1')
>>> 
>>> Decimal(1).exp()
Decimal('2.718281828459045235360287471')
>>> 
>>> Decimal(100).exp()
Decimal('2.688117141816135448412625552E+43')

Параметр context может принимать объект любого контекста вычислений.

from_float(f)
Преобразует число типа float в число типа Decimal как есть, т.е. так как оно представлено в памяти компьютера.

Некоторые числа типа float, не имеют точного представления в двоичной системе (являются бесконечными дробями) поэтому в памяти компьютера они всегда хранятся с некоторой погрешностью:

>>> from decimal import *
>>> 
>>> #  from_float() является методом класса Decimal:
... Decimal.from_float(0.24)
Decimal('0.2399999999999999911182158029987476766109466552734375')
>>> 
>>> Decimal.from_float(float('inf'))
Decimal('Infinity')
fma(other, third, context=None)
Умножает число на other и прибавляет к произведению число third, но без выполнения промежуточного округления.
>>> from decimal import *
>>> 
>>> Decimal(3).fma(4, 5)
Decimal('17')
>>> 
>>> Decimal('1.25').fma(Decimal('4'), Decimal('5.0000'))
Decimal('10.0000')

Параметр context может принимать объект любого контекста вычислений.

is_canonical()
Возвращает True если число находится в канонической форме, но так как числа типа Decimal всегда имеют каноническую форму то и метод всегда возвращает True.
>>> from decimal import *
>>> 
>>> Decimal('0.231').is_canonical()
True
is_finite()
Возвращает True если аргумент является конечным числом и False если бесконечностью или NaN.
>>> from decimal import *
>>> 
>>> Decimal('0.231').is_finite()
True
>>> 
>>> Decimal('NaN').is_finite()
False
is_infinite()
Возвращает True если аргумент является положительной или отрицательной бесконечностью и False во всех остальных случаях.
>>> from decimal import *
>>> 
>>> Decimal('inf').is_infinite()
True
>>> 
>>> Decimal('0.321').is_infinite()
False
is_nan()
Возвращает True если аргумент является NaN (или сигнальным sNaN) и False во всех остальных случаях.
>>> from decimal import *
>>> 
>>> Decimal('0.321').is_nan()
False
>>> 
>>> Decimal('nan').is_nan()
True
>>> Decimal('snan').is_nan()
True
is_normal(context=None)
Возвращает True если число является нормальным и False если число является нулем, субнормальным (слишком маленьким для текущей точности), бесконечностью или Nan.
>>> from decimal import *
>>> 
>>> Decimal(1).is_normal()
True
>>> Decimal('2.5').is_normal()
True
>>> Decimal('2.5e10').is_normal()
True
>>> Decimal('2.5E-10').is_normal()
True
>>> 
>>> 
>>> Decimal(0).is_normal()
False
>>> Decimal('0.00').is_normal()
False
>>> Decimal('1E-9999999').is_normal()
False
>>> Decimal('-Inf').is_normal()
False
>>> Decimal('NaN').is_normal()
False
is_qnan()
Возвращает True если аргумент является обычным NaN и False во всех остальных случаях.
>>> from decimal import *
>>> 
>>> Decimal('NaN').is_qnan()
True
>>> Decimal('sNaN').is_qnan()
False
>>> Decimal('0.1').is_qnan()
False
is_signed()
Возвращает True если аргумент имеет отрицательный знак и False во всех остальных случаях. Обратите внимание что ноль, NaN, sNaN и бесконечности так же могут иметь отрицательный знак.
>>> from decimal import *
>>> 
>>> Decimal('-0.1').is_signed(), Decimal('0.1').is_signed()
(True, False)
>>> 
>>> Decimal('-0').is_signed(), Decimal('0').is_signed()
(True, False)
>>> Decimal('-NaN').is_signed(), Decimal('NaN').is_signed()
(True, False)
>>> Decimal('-sNaN').is_signed(), Decimal('sNaN').is_signed()
(True, False)
>>> Decimal('-Inf').is_signed(), Decimal('Inf').is_signed()
(True, False)
is_snan()
Возвращает True если аргумент является sNaN (сигнальным NaN) и False во всех остальных случаях.
>>> from decimal import *
>>> 
>>> Decimal('sNaN').is_snan()
True
>>> 
>>> Decimal('NaN').is_snan()
False
is_subnormal()
Возвращает True если аргумент является субнормальным числом (слишком маленьким для текущей точности) и False во всех остальных случаях.
>>> from decimal import *
>>> 
>>> Decimal('1E-9999999').is_subnormal()
True
>>> Decimal('1E-999999').is_subnormal()
False
is_zero()
Возвращает True если аргумент является нулем (положительным или отрицательным) и False во всех остальных случаях.
>>> from decimal import *
>>> 
>>> Decimal('0').is_zero()
True
>>> Decimal('+0').is_zero()
True
>>> Decimal('-0').is_zero()
True
ln(context=None)
Возвращает натуральный логарифм числа. Результат округляется в режиме ROUND_HALF_EVEN.
>>> from decimal import *
>>> 
>>> Decimal('2.71').ln()
Decimal('0.9969486348916095320608871638')
>>> 
>>> Decimal('10').ln()
Decimal('2.302585092994045684017991455')

Параметр context может принимать объект любого контекста вычислений.

log10(context=None)
Возвращает десятичный логарифм числа. Результат округляется в режиме ROUND_HALF_EVEN.
>>> from decimal import *
>>> 
>>> Decimal('10').log10()
Decimal('1')
>>> 
>>> Decimal('101').log10()
Decimal('2.004321373782642574275188178')

Параметр context может принимать объект любого контекста вычислений.

logb(context=None)
Возвращает целое число, которое является показателем величины самой значимой цифры операнда.
>>> from decimal import *
>>> 
>>> Decimal('100').logb()
Decimal('2')
>>> Decimal('900').logb()
Decimal('2')
>>> 
>>> Decimal('999').logb()
Decimal('2')
>>> 
>>> Decimal('.01').logb()
Decimal('-2')
>>> Decimal('.0909').logb()
Decimal('-2')

Если операнд равен \(0\), то возвращается Decimal('-Infinity') и вызывается исключение DivisionByZero. Если операнд равен бесконечности, то так же возвращается бесконечность.

Параметр context может принимать объект любого контекста вычислений.

Логические операнды

Числа типа Decimal могут восприниматься как логические значения если их знак и экспонента равны \(0\), а все остальные цифры состоят только из \(0\) и \(1\). Например, число Decimal((0, (1, 1, 0, 1), 0)) или Decimal('1101') можно спокойно считать логическими и передавать их логическим функциям.

logical_and(other, context=None)
Логическое И двух операндов.
>>> from decimal import *
>>> 
>>> Decimal('1101').logical_and(Decimal('1001'))
Decimal('1001')

Параметр context может принимать объект любого контекста вычислений.

logical_invert(context=None)
Логическое НЕ (поразрядное инвертирование битов).
>>> from decimal import *
>>> 
>>> Decimal('10000').logical_invert()    #  не забываем про дополнительный код
Decimal('1111111111111111111111101111')

Параметр context может принимать объект любого контекста вычислений.

logical_or(other, context=None)
Логическое ИЛИ двух операндов.
>>> from decimal import *
>>> 
>>> Decimal('1010').logical_or(Decimal('101'))
Decimal('1111')

Параметр context может принимать объект любого контекста вычислений.

logical_xor(other, context=None)
Логическое ИСКЛЮЧАЮЩЕЕ ИЛИ двух операндов.
>>> from decimal import *
>>> 
>>> Decimal('1010').logical_xor(Decimal('1101'))
Decimal('111')

Параметр context может принимать объект любого контекста вычислений.

Сравнения "больше/меньше"

max(other, context=None)
Возвращает больший аргумент. Округление указанное в контексте выполняется перед возвращением результата.
>>> from decimal import *
>>> 
>>> Decimal('0.231').max(Decimal('0.2320'))
Decimal('0.2320')

Параметр context может принимать объект любого контекста вычислений.

min(other, context=None)
Возвращает меньший аргумент. Округление указанное в контексте выполняется перед возвращением результата.
>>> from decimal import *
>>> 
>>> Decimal('0.231').min(Decimal('0.2320'))
Decimal('0.231')

Параметр context может принимать объект любого контекста вычислений.

max_mag(other, context=None)
Возвращает больший аргумент, но сравниваются не сами аргументы, а их абсолютные значения. Округление указанное в контексте выполняется перед возвращением результата.
>>> from decimal import *
>>> 
>>> Decimal('0.231').max_mag(Decimal('-0.2320'))
Decimal('-0.2320')

Параметр context может принимать объект любого контекста вычислений.

min_mag(other, context=None)
Возвращает меньший аргумент, но сравниваются не сами аргументы, а их абсолютные значения. Округление указанное в контексте выполняется перед возвращением результата.
>>> from decimal import *
>>> 
>>> Decimal('0.231').min_mag(Decimal('-0.2320'))
Decimal('0.231')

Параметр context может принимать объект любого контекста вычислений.

Приращения аргументов

next_minus(context=None)
Возвращает наибольшее число в указанном контексте, которое меньше чем данный операнд.
>>> from decimal import *
>>> 
>>> Decimal('1').next_minus()
Decimal('0.9999999999999999999999999999')
next_plus(context=None)
Возвращает наименьшее число в указанном контексте, которое больше чем данный операнд.
>>> from decimal import *
>>> 
>>> Decimal('1').next_plus()
Decimal('1.000000000000000000000000001')
next_toward(other, context=None)
Возвращает минимально возможное число в указанном контексте, которое следует за указанным операндом в сторону числа other.
>>> from decimal import *
>>> 
>>> Decimal('1').next_toward(Decimal('2'))
Decimal('1.000000000000000000000000001')
>>> 
>>> Decimal('1').next_toward(Decimal('-2'))
Decimal('0.9999999999999999999999999999')
normalize(context=None)
Возвращает число к канонической форме.
>>> from decimal import *
>>> 
>>> Decimal('.001200')
Decimal('0.001200')
>>> 
>>> Decimal('.001200').normalize()
Decimal('0.0012')
>>> 
>>> Decimal('23.100000000000').normalize()
Decimal('23.1')
>>> 
>>> Decimal('.23100000000000e2').normalize()
Decimal('23.1')
number_class()
Возвращает строку описывающую класс операнда.
>>> from decimal import *
>>> 
>>> Decimal('+0').number_class()
'+Zero'
>>> 
>>> Decimal('-NaN').number_class()
'NaN'
quantize(exp, rounding=None, context=None)
Возвращает округленное значение первого операнда с показателем степени второго операнда.
>>> from decimal import *
>>> 
>>> Decimal('.0231231').quantize(Decimal('1.000'))
Decimal('0.023')

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

radix()
Возвращает Decimal('10') – основание системы счисления в которой выполняются все вычисления (присутствует только для соответствия спецификации).
>>> from decimal import *
>>> 
>>> Decimal('-NaN').radix()
Decimal('10')
>>> 
>>> Decimal('123').radix()
Decimal('10')
remainder_near()
Возвращает остаток от деления, но с таким знаком, который позволяет минимизировать его абсолютное значение.
>>> from decimal import *
>>> 
>>> Decimal('10').remainder_near(Decimal('6'))
Decimal('-2')
>>> Decimal('15').remainder_near(Decimal('4'))
Decimal('-1')
>>> Decimal('15').remainder_near(Decimal('7'))
Decimal('1')
rotate(other, context=None)
Выполняет смещение первого числа на число позиций, которое указано целым числом во втором операнде. Отрицательный знак второго операнда выполняет смещение вправо, положительный - влево. Смещение выполняется в пределах установленной в контексте точности.
>>> from decimal import *
>>> 
>>> Decimal('.123').rotate(Decimal('2'))
Decimal('12.300')
>>> 
>>> Decimal('.123').rotate(Decimal('-2'))
Decimal('2300000000000000000000000.001')
same_quantum(other, context=None)
Возвращает True если оба операнда имеют одинаковый показатель степени (или если оба NaN). Данный метод не зависит от контекста, не меняет флаги и не выполняет округление. Python реализованый на языке C может вызвать исключение InvalidOperation если второй операнд не имеет точного представления.
>>> from decimal import *
>>> 
>>> Decimal('2.31').same_quantum(Decimal('1.01'))
True
>>> 
>>> Decimal('2.31').same_quantum(Decimal('1.0100'))
False
>>> 
>>> Decimal('NaN').same_quantum(Decimal('NaN'))
True
scaleb(other, context=None)
Устанавливает показатель степени первого операнда в значение второго операнда (который должен быть целым числом).
>>> from decimal import *
>>> 
>>> Decimal('2.31').scaleb(Decimal('2'))
Decimal('231')
>>> 
>>> Decimal('2.31').scaleb(Decimal('-2'))
Decimal('0.0231')
shift(other, context=None)
Метод аналогичен функции rotate(), с тем лишь отличием, что смещение выполняется не циклически.
>>> from decimal import *
>>> 
>>> Decimal('2.3123').shift(Decimal('-3'))
Decimal('0.0023')
>>> Decimal('2.3123').shift(Decimal('3'))
Decimal('2312.3000')
>>> 
>>> Decimal('2.3123').shift(Decimal('5'))
Decimal('231230.0000')
>>> Decimal('2.3123').shift(Decimal('-5'))
Decimal('0.0000')
sqrt(context=None)
Возвращает квадратный корень числа.
>>> from decimal import *
>>> 
>>> Decimal('2').sqrt()
Decimal('1.414213562373095048801688724')
>>> 
>>> getcontext().prec = 100
>>> Decimal('2').sqrt()
Decimal('1.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573')
to_eng_string(context=None)
Возвращает строковое представление числа в инженерной форме.
>>> from decimal import *
>>> 
>>> Decimal('2.31003E-3').to_eng_string()
'0.00231003'
>>> 
>>> Decimal('231003E3').to_eng_string()
'231.003E+6'
to_integral(rounding=None, context=None)
Эквивалентен методу to_integral_value() и присутствует только для соответствия спецификации.
to_integral_exact(rounding=None, context=None)
Выполняет округление до ближайшего целого числа и может выдавать сообщения о том была ли операция точной (Inexact) и выполнялось ли округление (Rounded). Округление выполняется в режиме который можно указать в параметре rounding, если не один параметр не указан, то округление выполняется в режиме текущего контекста.
>>> from decimal import *
>>> 
>>> Decimal('2.51').to_integral_exact()
Decimal('3')
>>> 
>>> Decimal('2.50').to_integral_exact()
Decimal('2')
to_integral_value(rounding=None, context=None)
Метод аналогичен to_integral_exact(), с тем лишь отличием, что сообщения о неточности операции и факта округления никогда не выводятся.
>>> from decimal import *
>>> 
>>> Decimal('2.50').to_integral_value()
Decimal('2')
>>> Decimal('2.51').to_integral_value()
Decimal('3')

Объекты контекста

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

Каждый поток имеет свой собственный контекст, который можно изменить или установить с помощью функций getcontext() или setcontext()

decimal.getcontext()
Возвращает текущий контекст, а так же предоставляет доступ к его отдельным параметрам.
>>> from decimal import *
>>> 
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
>>> 
>>> getcontext().prec = 1000
>>> 
>>> getcontext()
Context(prec=1000, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
class decimal.Context(prec=None, rounding=None, Emin=None, Emax=None, capitals=None, clamp=None, flags=None, traps=None)
Создает новый контекст.
>>> from decimal import *
>>> 
>>> my_context = Context(prec = 9, Emax = 99999)
>>> setcontext(my_context)
>>> getcontext()
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=99999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])

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

prec — это целое число, которое задает точность арифметических операций в контексте. Данное число может принимать значения из интервала [0, MAX_PREC].

rounding позволяет задать режим округления.

Поля traps и flags являются списками с сигналами и статусами их отслеживания.

Поля Emin и Emax являются целыми числами, задающими пределы для показателя степени. Значения Emin лежат в диапазоне [MIN_EMIN, 0], а значения Emax в диапазоне [0, MAX_EMAX].

Поле capitals определяет как выводить символ степени. По умолчанию это значение установлено в \(1\), что соответствует использованию заглавной буквы E, а \(0\) приводит к использованию строчной буквы e.

Поле clamp может быть равно либо \(1\) либо \(0\) по умолчанию. Если установлено в \(1\), экспонента - e десятичного числа в текущем контексте может находиться только в интервале [Emin - prec + 1, Emax - prec + 1]. Если clamp = 0 то показатель степени просто не превосходит Emax. Когда clamp = 1 то его показатель степени будет уменьшаться а к коэффициенту добавляться соответствующее число нулей, таким образом это позволяет соответствовать ограничениям показателям степени, но приводит к потере информации о количестве значащих нулей справа:

>>> from decimal import *
>>> 
>>> Context(prec=10, Emax=999, clamp=1).create_decimal('7.77e999')
Decimal('7.770000000E+999')

Значение clamp = 1 обеспечивает совместимость с числами с плавающей точкой фиксированной длинны, которые указаны в IEEE 754.

decimal.setcontext(c)
Позволяет установить необходимый контекст для текущего потока.
>>> from decimal import *
>>> 
>>> my_context = Context(prec = 5)
>>> setcontext(my_context)

Что бы временно изменить контекст можно воспользоваться оператором with и функцией localcontext().

decimal.localcontext(ctx=None)
Возвращает менеджер контекста with, который установит текущий контекст для активного потока в копию ctx при входе в with и восстановит предыдущий контекст при выходе из него. Если контекст не указан то используется текущий контекст.
>>> from decimal import *
>>> 
>>> getcontext().prec = 5    #  устанавливаем точность текущего контекста
>>> 
>>> with localcontext() as ctx:
...     ctx.prec = 50    #  устанавливаем большую точность
...     x = Decimal(2).sqrt()
... 
>>> x
Decimal('1.4142135623730950488016887242096980785696718753769')
>>> 
>>> x = +x    #  выполняем округление в пределах текущей точности
>>> x
Decimal('1.4142')
class decimal.BasicContext
Возвращает .
>>> from decimal import *
>>> 
class decimal.ExtendedContext
Возвращает .
>>> from decimal import *
>>> 
class decimal.DefaultContext
Возвращает .
>>> from decimal import *
>>> 

Методы класса Context

Так же как и класс Decimal, класс Context определяет все теже самые методы (кроме методов adjusted() и as_tuple()). Например, если у нас имеется экземпляр контекста my_context, то выполнение экспоненциирования, как метода класса Decimal будет эквивалентно выполнению этой же операции как метода класса Context:

>>> Decimal('10').exp(context = my_context)
Decimal('22026')
>>> 
>>> my_context.exp(10)
Decimal('22026')

Методы класса Context также как и методы класса Decimal могут принимать числа типа int.

func()
Возвращает .
>>> from decimal import *
>>>