Cuando trabajamos con números decimales en Python generalmente recurrimos a la clase interna float, y es que además, cuando escribimos el número flotante de manera literal, Python nos devuelve un objecto de tipo float en vez de un objecto de tipo Decimal. Esto nos va a funcionar para la mayoría de los casos, pero los números float tienen limitaciones que podrían ser problemáticas para otros casos de uso. Simplemente no son lo suficientemente precisos.
Decimal vs flotante en Python
El problema de la aritmética de Punto Flotante
Los números de punto flotante se representan en el hardware de la computadora en fracciones en base 2 (binario). Por ejemplo, la fracción decimal
0.125
…tiene el valor 1/10 + 2/100 + 5/1000, y de la misma manera la fracción binaria
0.001
…tiene el valor 0/2 + 0/4 + 1/8. Estas dos fracciones tienen valores idénticos, la única diferencia real es que la primera está escrita en notación fraccional en base 10 y la segunda en base 2.
Lamentablemente, la mayoría de las fracciones decimales no pueden representarse exactamente como fracciones binarias. Como consecuencia, en general los números de punto flotante decimal que ingresas en tu computador son sólo aproximados por los números de punto flotante binario que realmente se guardan en la máquina.
El problema es más fácil de entender primero en base 10. Considerá la fracción 1/3. Podés aproximarla como una fracción de base 10
0.3
…o, mejor,
0.33
…o, mejor,
0.333
…y así. No importa cuantos dígitos desees escribir, el resultado nunca será exactamente 1/3, pero será una aproximación cada vez mejor de 1/3.
De la misma manera, no importa cuantos dígitos en base 2 quieras usar, el valor decimal 0.1 no puede representarse exactamente como una fracción en base 2. En base 2, 1/10 es la siguiente fracción que se repite infinitamente:
0.0001100110011001100110011001100110011001100110011...
La mayoría de los usuarios no son conscientes de esta aproximación por la forma en que se muestran los valores. Python solamente muestra una aproximación decimal al valor verdadero decimal de la aproximación binaria almacenada por la máquina. En la mayoría de las máquinas, si Python fuera a imprimir el verdadero valor decimal de la aproximación binaria almacenada para 0.1, debería mostrar
>>> print(f"{0.1:.55f}")
0.1000000000000000055511151231257827021181583404541015625
Esos son más dígitos que lo que la mayoría de la gente encuentra útil, por lo que Python mantiene manejable la cantidad de dígitos al mostrar en su lugar un valor redondeado.
Un problema práctico
Tenemos los siguientes números:
a = 10
b = a / 77
c = b * 77
en el ejemplo anterior, a debería ser igual a c, pero no lo es ya que sus valores son los siguientes:
a = 10
b = 0.12987012987012986
c = 9.999999999999998
El módulo decimal
Afortunadamente Python tiene otra forma de lidiar con los números decimales, que es el módulo decimal. A diferencia de los flotantes, los objectos Decimal definidos en el módulo decimal no son propensos a esta pérdida de precisión, porque no se basan en fracciones binarias.
from decimal import Decimal
>>> Decimal(0.1)
0.1000000000000000055511151231257827021181583404541015625
¡Ups! En el ejemplo anterior hemos obtenido un número con toda la impresición del float. Esto se debe a que Python primero evaluó el flotante 0.1, de modo que el valor que estuvo obteniendo el objeto Decimal en realidad fue este
Decimal(0.1000000000000000055511151231257827021181583404541015625)
La manera correcta de hacer esto es pasandole una representación, del número que queremos utilizar, en string.
>>> n = Decimal("0.1")
>>> f"{n:.60f}"
'0.100000000000000000000000000000000000000000000000000000000000'
>>> n = Decimal(str(0.1))
>>> f"{n:.60f}"
'0.100000000000000000000000000000000000000000000000000000000000'
¡Problema resuelto!
Si quieres saber más sobre este tema te recomiendo
decimal- Aritmética decimal de coma fija y coma flotante
Decimal vs float in Python