decimal — Matemáticas de coma fija y flotante

Propósito:Aritmética decimal usando números de coma fija y flotante

El módulo decimal implementa aritmética de coma fija y flotante usando el modelo familiar para la mayoría de las personas, en lugar de la versión de coma flotante IEEE implementada por la mayoría de hardware y familiar para los programadores. Una instancia de Decimal puede representar cualquier número exactamente, redondear hacia arriba o hacia abajo, y aplicar un límite al número de dígitos significativos

Decimal

Los valores decimales se representan como instancias de la clase Decimal. El constructor toma como argumento un entero o una cadena. Los números de coma flotante se pueden convertir en una cadena antes de ser utilizados para crear un Decimal, dejando que la persona que invoca trate explícitamente con la cantidad de dígitos para los valores que no se pueden expresar exactamente usando representaciones de coma flotante de hardware. Alternativamente, el método de clase from_float() convierte a la representación exacta decimal.

decimal_create.py
import decimal

fmt = '{0:<25} {1:<25}'

print(fmt.format('Input', 'Output'))
print(fmt.format('-' * 25, '-' * 25))

# Integer
print(fmt.format(5, decimal.Decimal(5)))

# String
print(fmt.format('3.14', decimal.Decimal('3.14')))

# Float
f = 0.1
print(fmt.format(repr(f), decimal.Decimal(str(f))))
print('{:<0.23g} {:<25}'.format(
    f,
    str(decimal.Decimal.from_float(f))[:25])
)

El valor del coma flotante de 0.1 no se representa como un valor exacto en binario, por lo que la representación como un float es diferente del valor de Decimal. La cadena de representación completa se trunca a 25 caracteres en la última línea de esta salida.

$ python3 decimal_create.py

Input                     Output
------------------------- -------------------------
5                         5
3.14                      3.14
0.1                       0.1
0.10000000000000000555112 0.10000000000000000555111

Decimals también se puede crear a partir de tuplas que contienen un indicador del signo (0 para positivo, 1 para negativo), un tuple de dígitos, y un exponente entero.

decimal_tuple.py
import decimal

# Tuple
t = (1, (1, 1), -2)
print('Input  :', t)
print('Decimal:', decimal.Decimal(t))

La representación basada en tuplas es menos conveniente de crear, pero ofrece una forma portátil de exportar valores decimales sin perder precisión. La forma de tupla se puede transmitir a través de la red o ser almacenada en una base de datos que no admite valores decimales precisos, y ser convertidos de vuelta a una instancia de Decimal más tarde.

$ python3 decimal_tuple.py

Input  : (1, (1, 1), -2)
Decimal: -0.11

Formateo

Decimal responde al protocolo de formato de cadena de Python, usando la misma sintaxis y opciones que otros tipos numéricos.

decimal_format.py
import decimal

d = decimal.Decimal(1.1)
print('Precision:')
print('{:.1}'.format(d))
print('{:.2}'.format(d))
print('{:.3}'.format(d))
print('{:.18}'.format(d))

print('\nWidth and precision combined:')
print('{:5.1f} {:5.1g}'.format(d, d))
print('{:5.2f} {:5.2g}'.format(d, d))
print('{:5.2f} {:5.2g}'.format(d, d))

print('\nZero padding:')
print('{:05.1}'.format(d))
print('{:05.2}'.format(d))
print('{:05.3}'.format(d))

Las cadenas de formato pueden controlar el ancho de la salida, la precisión (número de dígitos significativos) y cómo rellenar el valor para llenar el ancho.

$ python3 decimal_format.py

Precision:
1
1.1
1.10
1.10000000000000009

Width and precision combined:
  1.1     1
 1.10   1.1
 1.10   1.1

Zero padding:
00001
001.1
01.10

Aritmética

Decimal sobrecarga los operadores aritméticos simples por lo que las instancias pueden ser manipuladas de la misma manera que los tipos numéricos incorporados.

decimal_operators.py
import decimal

a = decimal.Decimal('5.1')
b = decimal.Decimal('3.14')
c = 4
d = 3.14

print('a     =', repr(a))
print('b     =', repr(b))
print('c     =', repr(c))
print('d     =', repr(d))
print()

print('a + b =', a + b)
print('a - b =', a - b)
print('a * b =', a * b)
print('a / b =', a / b)
print()

print('a + c =', a + c)
print('a - c =', a - c)
print('a * c =', a * c)
print('a / c =', a / c)
print()

print('a + d =', end=' ')
try:
    print(a + d)
except TypeError as e:
    print(e)

Los operadores Decimal también aceptan argumentos enteros, pero los valores de coma flotante se deben convertir a instancias Decimal.

$ python3 decimal_operators.py

a     = Decimal('5.1')
b     = Decimal('3.14')
c     = 4
d     = 3.14

a + b = 8.24
a - b = 1.96
a * b = 16.014
a / b = 1.624203821656050955414012739

a + c = 9.1
a - c = 1.1
a * c = 20.4
a / c = 1.275

a + d = unsupported operand type(s) for +: 'decimal.Decimal' and
 'float'

Más allá de la aritmética básica, Decimal incluye los métodos para encontrarla base 10 y logaritmos naturales. Los valores de retorno de log10() y ln() son instancias de Decimal, por lo que se puede usar directamente en fórmulas con otros valores.

Valores especiales

Además de los valores numéricos esperados, Decimal puede representar varios valores especiales, incluidos los valores positivos y negativos para infinito, «no un número», y cero.

decimal_special.py
import decimal

for value in ['Infinity', 'NaN', '0']:
    print(decimal.Decimal(value), decimal.Decimal('-' + value))
print()

# Math with infinity
print('Infinity + 1:', (decimal.Decimal('Infinity') + 1))
print('-Infinity + 1:', (decimal.Decimal('-Infinity') + 1))

# Print comparing NaN
print(decimal.Decimal('NaN') == decimal.Decimal('Infinity'))
print(decimal.Decimal('NaN') != decimal.Decimal(1))

Agregar a valores infinitos devuelve otro valor infinito. Comparar para la igualdad con NaN siempre devuelve falso y la comparación de la desigualdad siempre regresa verdadero. Comparando para el orden de clasificación contra NaN no está definido y genera un error.

$ python3 decimal_special.py

Infinity -Infinity
NaN -NaN
0 -0

Infinity + 1: Infinity
-Infinity + 1: -Infinity
False
True

Contexto

Hasta ahora, todos los ejemplos han utilizado los comportamientos predeterminados del módulo decimal. Es posible anular configuraciones como la precisión mantenida, cómo se realiza el redondeo, el manejo de errores, etc. usando un contexto. Los contextos se pueden aplicar para todas las instancias Decimal en un hilo o localmente dentro de una región pequeña de código.

Contexto actual

Para recuperar el contexto global actual, usa getcontext.

decimal_getcontext.py
import decimal

context = decimal.getcontext()

print('Emax     =', context.Emax)
print('Emin     =', context.Emin)
print('capitals =', context.capitals)
print('prec     =', context.prec)
print('rounding =', context.rounding)
print('flags    =')
for f, v in context.flags.items():
    print('  {}: {}'.format(f, v))
print('traps    =')
for t, v in context.traps.items():
    print('  {}: {}'.format(t, v))

Este script de ejemplo muestra las propiedades públicas de un Context.

$ python3 decimal_getcontext.py

Emax     = 999999
Emin     = -999999
capitals = 1
prec     = 28
rounding = ROUND_HALF_EVEN
flags    =
  <class 'decimal.InvalidOperation'>: False
  <class 'decimal.FloatOperation'>: False
  <class 'decimal.DivisionByZero'>: False
  <class 'decimal.Overflow'>: False
  <class 'decimal.Underflow'>: False
  <class 'decimal.Subnormal'>: False
  <class 'decimal.Inexact'>: False
  <class 'decimal.Rounded'>: False
  <class 'decimal.Clamped'>: False
traps    =
  <class 'decimal.InvalidOperation'>: True
  <class 'decimal.FloatOperation'>: False
  <class 'decimal.DivisionByZero'>: True
  <class 'decimal.Overflow'>: True
  <class 'decimal.Underflow'>: False
  <class 'decimal.Subnormal'>: False
  <class 'decimal.Inexact'>: False
  <class 'decimal.Rounded'>: False
  <class 'decimal.Clamped'>: False

Precisión

El atributo prec del contexto controla la precisión mantenida para nuevos valores creados como resultado de la aritmética. Los valores literales son mantenidos como se describe.

decimal_precision.py
import decimal

d = decimal.Decimal('0.123456')

for i in range(1, 5):
    decimal.getcontext().prec = i
    print(i, ':', d, d * 1)

Para cambiar la precisión, asigna un nuevo valor entre 1 y decimal.MAX_PREC directamente al atributo.

$ python3 decimal_precision.py

1 : 0.123456 0.1
2 : 0.123456 0.12
3 : 0.123456 0.123
4 : 0.123456 0.1235

Redondeo

Hay varias opciones para el redondeo para mantener los valores dentro de la precisión deseada.

ROUND_CEILING
Siempre redondea hacia arriba hacia el infinito.
ROUND_DOWN
Siempre redondea hacia cero.
ROUND_FLOOR
Siempre redondea hacia el infinito negativo.
ROUND_HALF_DOWN
Redondea desde cero si el último dígito significativo es mayor que o igual a 5, de lo contrario hacia cero.
ROUND_HALF_EVEN
Como ROUND_HALF_DOWN excepto que si el valor es 5, entonces el dígito precedente es examinado. Los valores pares hacen que el resultado sea redondeado hacia abajo y los dígitos impares hacen que el resultado sea redondeado hacia arriba.
ROUND_HALF_UP
Como ROUND_HALF_DOWN excepto que si el último dígito significativo es 5, el valor se redondea desde cero.
ROUND_UP
Redondea desde cero.
ROUND_05UP
Redondea desde cero si el último dígito es 0 o 5, de lo contrario hacia cero.
decimal_rounding.py
import decimal

context = decimal.getcontext()

ROUNDING_MODES = [
    'ROUND_CEILING',
    'ROUND_DOWN',
    'ROUND_FLOOR',
    'ROUND_HALF_DOWN',
    'ROUND_HALF_EVEN',
    'ROUND_HALF_UP',
    'ROUND_UP',
    'ROUND_05UP',
]

header_fmt = '{:10} ' + ' '.join(['{:^8}'] * 6)

print(header_fmt.format(
    ' ',
    '1/8 (1)', '-1/8 (1)',
    '1/8 (2)', '-1/8 (2)',
    '1/8 (3)', '-1/8 (3)',
))
for rounding_mode in ROUNDING_MODES:
    print('{0:10}'.format(rounding_mode.partition('_')[-1]),
          end=' ')
    for precision in [1, 2, 3]:
        context.prec = precision
        context.rounding = getattr(decimal, rounding_mode)
        value = decimal.Decimal(1) / decimal.Decimal(8)
        print('{0:^8}'.format(value), end=' ')
        value = decimal.Decimal(-1) / decimal.Decimal(8)
        print('{0:^8}'.format(value), end=' ')
    print()

Este programa muestra el efecto de redondear el mismo valor a diferentes niveles de precisión usando los diferentes algoritmos.

$ python3 decimal_rounding.py

           1/8 (1)  -1/8 (1) 1/8 (2)  -1/8 (2) 1/8 (3)  -1/8 (3)
CEILING      0.2      -0.1     0.13    -0.12    0.125    -0.125

DOWN         0.1      -0.1     0.12    -0.12    0.125    -0.125

FLOOR        0.1      -0.2     0.12    -0.13    0.125    -0.125

HALF_DOWN    0.1      -0.1     0.12    -0.12    0.125    -0.125

HALF_EVEN    0.1      -0.1     0.12    -0.12    0.125    -0.125

HALF_UP      0.1      -0.1     0.13    -0.13    0.125    -0.125

UP           0.2      -0.2     0.13    -0.13    0.125    -0.125

05UP         0.1      -0.1     0.12    -0.12    0.125    -0.125

Contexto local

El contexto se puede aplicar a un bloque de código usando la declaración with.

decimal_context_manager.py
import decimal

with decimal.localcontext() as c:
    c.prec = 2
    print('Local precision:', c.prec)
    print('3.14 / 3 =', (decimal.Decimal('3.14') / 3))

print()
print('Default precision:', decimal.getcontext().prec)
print('3.14 / 3 =', (decimal.Decimal('3.14') / 3))

El Context admite la interfaz de gestor de contexto utilizada por with, por lo que la configuración solo se aplica dentro del bloque.

$ python3 decimal_context_manager.py

Local precision: 2
3.14 / 3 = 1.0

Default precision: 28
3.14 / 3 = 1.046666666666666666666666667

Contexto por instancia

Los contextos también se pueden usar para construir instancias Decimal, que luego heredan los argumentos de precisión y redondeo a la conversión del contexto.

decimal_instance_context.py
import decimal

# Set up a context with limited precision
c = decimal.getcontext().copy()
c.prec = 3

# Create our constant
pi = c.create_decimal('3.1415')

# The constant value is rounded off
print('PI    :', pi)

# The result of using the constant uses the global context
print('RESULT:', decimal.Decimal('2.01') * pi)

Esto permite que una aplicación seleccione la precisión de los valores constantes por separado de la precisión de los datos del usuario, por ejemplo.

$ python3 decimal_instance_context.py

PI    : 3.14
RESULT: 6.3114

Hilos

El contexto «global» es en realidad local de hilos, por lo que cada hilo puede potencialmente ser configurado usando diferentes valores.

decimal_thread_context.py
import decimal
import threading
from queue import PriorityQueue


class Multiplier(threading.Thread):
    def __init__(self, a, b, prec, q):
        self.a = a
        self.b = b
        self.prec = prec
        self.q = q
        threading.Thread.__init__(self)

    def run(self):
        c = decimal.getcontext().copy()
        c.prec = self.prec
        decimal.setcontext(c)
        self.q.put((self.prec, a * b))


a = decimal.Decimal('3.14')
b = decimal.Decimal('1.234')
# A PriorityQueue will return values sorted by precision,
# no matter what order the threads finish.
q = PriorityQueue()
threads = [Multiplier(a, b, i, q) for i in range(1, 6)]
for t in threads:
    t.start()

for t in threads:
    t.join()

for i in range(5):
    prec, value = q.get()
    print('{}  {}'.format(prec, value))

Este ejemplo crea un nuevo contexto usando el especificado, luego lo instala dentro de cada hilo.

$ python3 decimal_thread_context.py

1  4
2  3.9
3  3.87
4  3.875
5  3.8748

Ver también