Функции с числами Decimal. Глобальный и локальный контенты

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Смотреть материал на YouTube | RuTube

На прошлом занятии мы с вами познакомились с различными методами класса Decimal. Однако к объектам этого класса применимы и обычные стандартные функции языка Python. Пусть для примера у нас имеются следующие объекты:

a = Decimal('-7.8')
pi = Decimal('3.1415')

Тогда функция round() возвратит следующие значения:

round(pi) # 3
round(pi, 3) # Decimal('3.142')

Обратите внимание, что для вещественного значения возвращается не просто число типа float, а объект класса Decimal. В действительности в классе Decimal переопределены многие магические методы, которые вызываются при использовании стандартных функций, типа round() или abs():

abs(a)  # Decimal('7.8')

Именно поэтому эти функции «знают» как обрабатывать объекты этого класса.

Те из вас, кто проходил курс по ООП Python, должны прекрасно все это понимать. Например, если определить некоторый класс, пусть у него будет имя A, с магическими методами __round__() и __abs__():

class A:
    def __round__(self, n=None):
        print("round")
        return 0
 
    def __abs__(self):
        print("abs")
        return 1

То при вызове стандартных функций round() и abs() с объектами класса A:

var_a = A()
res1 = round(var_a)  # 0
res2 = abs(var_a)  # 1

будут вызваны соответствующие магические методы объекта var_a. По этому же принципу эти функции работают с объектами класса Decimal.

Следует отметить, что алгоритм округления с применением функции round() работает по принципу, заложенному в магическом методе  __round__() и отличается от реализации ранее рассмотренного метода quantize(). Поэтому они могут выдавать разные результаты в зависимости от выбранного режима округления.

Помимо функций round() и abs() с объектами класса Decimal можно использовать и некоторые другие встроенные функции языка Python. Например, пусть имеется список из значений температур:

tmp = [Decimal('23.5'), Decimal('24.1'), Decimal('23.9')]

Тогда с ним возможны следующие операции:

sum(tmp)  # Decimal('71.5')
tuple(filter(lambda x: x < 24, tmp))  # (Decimal('23.5'), Decimal('23.9'))
list(map(float, tmp))  # [23.5, 24.1, 23.9]
tmp.sort()  # [Decimal('23.5'), Decimal('23.9'), Decimal('24.1')]
max(tmp)  # Decimal('24.1')
min(tmp)  # Decimal('23.5')

Помимо использования встроенных функций с объектами класса Decimal формально можно применять функции математической библиотеки math, которую мы ранее с вами уже рассматривали. Например:

from decimal import Decimal
import math
 
a = Decimal('-7.8')
pi = Decimal('3.1415')
 
math.floor(a)   # -8
math.ceil(a)  # -7
math.pow(a, 2)  # 60.839999999999996
math.sqrt(pi)  # 1.772427713617681

Однако они обрабатывают значения объектов Decimal, как обычные вещественные числа типа float. В действительности, в них сначала происходит неявное преобразование типов:

val_a = float(a)  # -7.8
val_pi = float(pi) # 3.1415

А затем, стандартное выполнение функции модуля math с вещественным типом данных. Именно поэтому на выходе мы видим обычные вещественные числа, а не объекты класса Decimal. По этой же причине в вычислениях присутствуют неточности, погрешности. В частности значение:

math.pow(a, 2)  # 60.839999999999996

должно быть:

a ** 2  # Decimal('60.84')

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

pi.sqrt()  # Decimal('1.772427713617680939696091927')
abs(a).log10()  # Decimal('0.8920946026904804017152719559')

и так далее.

Возможно, у вас здесь возникает вопрос: если класс Decimal обеспечивает более высокую точность вычислений, то почему бы его не использовать вместо стандартного типа float везде и всюду? Но тут, как всегда, у любого положительного момента имеются отрицательные стороны. Главной из них является заметно более низкая скорость вычислений с объектами класса Decimal в сравнении с вещественными числами типа float. Это является платой за увеличение точности вычислений. Поэтому везде, где можно мириться с микроскопическими погрешностями, используют тип float. А в редких случаях, где это недопустимо – объекты класса Decimal.

Контексты класса Decimal

До сих пор, когда мы с вами в программе выполняли различные операции с числами класса Decimal, то они отрабатывали в рамках заданного глобального контекста, который контролирует точность вещественных чисел, режим округления, способ обработки ошибок. Для получения текущего глобального контекста используется функция getcontext() модуля decimal:

from decimal import Decimal
import decimal
 
context = decimal.getcontext()
print(context)

После выполнения в консоли увидим следующую информацию:

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

Здесь свойства:

  • context.prec – предельная точность вещественных чисел при арифметических операциях (28 знаков);
  • context.rounding – режим округления;
  • context.Emin, context.Emax – минимальное и максимальное значения экспоненты (степень десятки);
  • context.capitals – способ отображения символа экспоненты: 0 – малая буква ‘e’; 1 – заглавная буква ‘E’;
  • context.clamp – принимает два значения 0 или 1 и влияет на диапазон представления вещественных чисел;
  • context.flags, context.traps – списки (словари) сигналов (исключений), которые могут генерировать объекты класса Decimal.

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

context = decimal.getcontext()
context.prec = 4

Тогда команда:

d = Decimal('123.456789')
print(d * 1)  # 123.5

отобразит значение 123.5 – ровно четыре знака. Обратите внимание, что свойство prec применяется именно при арифметических операциях. Именно поэтому в функции print() записано произведение числа d на 1.

В общем случае имеем следующее поведение свойства prec:

d = Decimal('123.456789')
for i in range(1, 10):
    context.prec = i
    print(i, ':', d * 1)

Получим:

1 : 1E+2  # 100
2 : 1.2E+2  # 120
3 : 123
4 : 123.5
5 : 123.46
6 : 123.457
7 : 123.4568
8 : 123.45679
9 : 123.456789

Видим, что при точности 1 имеем одну значащую цифру, при точности 2 – две значащих цифры, и так далее.

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

context = decimal.getcontext()
context.rounding = decimal.ROUND_FLOOR
 
var = Decimal('56.72564')
res = var.quantize(Decimal('0.00'))
print(res)

Увидим значение 56.73.

Далее, в словаре traps глобального контекста мы видим исключения, которые генерируются объектами класса Decimal:

[InvalidOperation, DivisionByZero, Overflow]

Поэтому команда:

res = var / 0  # DivisionByZero

приведет к ошибке типа DivisionByZero. Но мы можем это изменить, выключив данный тип исключения. Сделать это можно следующей командой:

context.traps[decimal.DivisionByZero] = False

Обратите внимание, что мы здесь обращаемся к типу исключения по ключу и задаем для него значение False. После этого команда:

res = var / 0  # Infinity

вернет значение Infinity (бесконечность).

Список всех возможных исключений (сигналов) можно отобразить командой:

for k, v in context.traps.items():
    print(k, '=', v)

Получим:

<class 'decimal.InvalidOperation'> = True
<class 'decimal.FloatOperation'> = False
<class 'decimal.DivisionByZero'> = False
<class 'decimal.Overflow'> = True
<class 'decimal.Underflow'> = False
<class 'decimal.Subnormal'> = False
<class 'decimal.Inexact'> = False
<class 'decimal.Rounded'> = False
<class 'decimal.Clamped'> = False

Использование локального контекста

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

context = decimal.getcontext()  # глобальный контекст
context.rounding = decimal.ROUND_FLOOR
 
var = Decimal('56.72564')
 
with decimal.localcontext() as ctx:  # локальный контекст
    ctx.traps[decimal.DivisionByZero] = False
 
    d = int(input())
    res = var / d
 
print(res)

Смотрите, в программе вначале настраивается глобальный контекст context, а ниже – локальный ctx, в котором отключается генерация ошибки DivisionByZero. Теперь, если пользователь введет нуль с клавиатуры, то переменная res примет значение Infinity и программа продолжит свою работу.

Методы контекста (класса Context)

Глобальный и локальный контексты имеют примерно тот же набор методов, что и класс Decimal. Например:

context = decimal.getcontext()
context.rounding = decimal.ROUND_FLOOR
 
a = Decimal(4)
b = Decimal('-5.415')
 
r1 = context.sqrt(a)
r2 = context.power(b, a)
r3 = context.quantize(b, Decimal('0.00'))
print(r1, r2, r3, sep='\n')

Получим результаты:

2
859.792878950625
-5.42

Их отличие лишь в том, что в эти методы дополнительно нужно передавать один или несколько объектов класса Decimal, к которым они применяются. Во всем остальном их поведение аналогично. Но дополнительно класс Context имеет метод copy(), позволяющий создавать копию контекста. Например:

ctx = decimal.getcontext().copy()

Затем, можно настроить этот контекст по своему желанию, например, так:

ctx.prec = 5
ctx.rounding = decimal.ROUND_HALF_DOWN

И в рамках этого контекста сформировать объект класса Decimal:

a = ctx.create_decimal('-56.7834025')

Теперь, при вычислениях, Python будет автоматически выбирать наиболее подходящий контекст для формирования итоговых результатов:

print(a * 1)  # -56.783 (контекст ctx)
print(a * Decimal('-5.12345678'))  # 290.92524633874 (глобальный контекст)

Создание собственного контекста

Наконец, мы можем с помощью класса Context создать свой собственный объект контекста и использовать его. Например, так:

custom_ctx = Context(prec=4, rounding='ROUND_HALF_UP', traps=[decimal.DivisionByZero])
decimal.setcontext(custom_ctx)

Здесь функция setcontext() задает текущий глобальный контекст на основе объекта custom_ctx. После этого все арифметические операции будут по умолчанию использовать настройки из custom_ctx:

a = Decimal('-54.35646032')
b = Decimal('74.35')
print(a + b)  # 19.99

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

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Видео по теме