Шаг 46.
Python: сборник рецептов.
Числа, даты и время. Выполнение точных вычислений с десятичными дробями

    На этом шаге мы рассмотрим использование модуля decimal.

Задача

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

Решение

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

>>> a = 4.2 
>>> b = 2.1
>>> a + b 
6.300000000000001  
>>> (a + b) == 6.3 
False 
>>>

    Эти ошибки - "особенность" процессора и стандарта представления чисел с плавающей точкой IEEE 754, на основе которого работает модуль процессора для вычислений с плавающей точкой. Поскольку в типе данных "числа с плавающей точкой" Python хранит данные, используя нативное представление, вы ничего не можете сделать, чтобы избавиться от ошибок при использовании экземпляров float.

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

>>> from decimal import Decimal 
>>> a = Decimal('4.2')
>>> b = Decimal('2.1')
>>> a + b 
Decimal('6.3')
>>> print(a + b)
6.3
>>> (a + b) == Decimal('6.3')
True
>>>

    На первый взгляд он может показаться странным (например, определение чисел как строк). Однако объекты Decimal работают именно так, как вы можете ожидать (поддерживают все обычные математические операции и т. д.). Если вы выводите их или используете в функциях форматирования строк, они выглядят как обычные числа.

    Главное преимущество decimal в том, что он позволяет контролировать различные аспекты вычислений, такие как число знаков после точки и округление. Чтобы это сделать, вы создаете локальный контекст и меняете его установки. Например:

>>> from decimal import localcontext
>>> a = Decimal('1.3')
>>> b = Decimal('1.7')
>>> print(a / b)
0.7647058823529411764705882353
>>> with localcontext() as ctx:
	ctx.prec = 3
	print(a / b)

	
0.765
>>> with localcontext() as ctx:
	ctx.prec = 50
	print(a / b)

	
0.76470588235294117647058823529411764705882352941176
>>> 


Обсуждение

    Модуль decimal реализует "Общую спецификацию десятичной арифметики" ("General Decimal Arithmetic Specification") компании IBM. Нет нужды упоминать, что у него есть очень много различных опций для конфигурирования, описание которых лежит за пределами возможностей данного изложения.

    Новички в Python могут склоняться к повсеместному использованию модуля decimal для решения проблемы неточности, которая неизбежна при работе с типом данных float. Однако важно понимать область применения вашего приложения. Если вы работаете с научными или инженерными данными, компьютерной графикой, то вполне нормально использовать обычный тип данных чисел с плавающей точкой. В общем-то, очень немногие вещи в реальном мире измеряются с точностью до 17-го знака после точки, которую предоставляет float. Так что небольшие ошибки не так уж важны. А производительность нативных чисел с плавающей точкой заметно выше, что важно при выполнении большого количества вычислений.

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

>>> nums = [1.23e+18, 1, -1.23e+18]
>>> sum(nums) # Заметьте, как исчезает 1
0.0
>>> 

    Ошибка из последнего примера может быть решена путем использования math.fsum():

>>> import math 
>>> math.fsum(nums)
1.0
>>>

    Однако для других алгоритмов вам придется изучить реализацию и понять, как они работают с точки зрения подобных ошибок.

    Подведем итог: модуль decimal используется в основном в финансовых и прочих подобных приложениях. В таких программах небольшие ошибки в вычислениях ужасно мешают, а decimal позволяет от них избавиться. Также часто можно встретить объекты класса Decimal в интерфейсах Python к базам данных - опять же, особенно часто их используют для доступа к финансовым данным.

    На следующем шаге мы рассмотрим форматирование чисел для вывода.




Предыдущий шаг Содержание Следующий шаг