Subsecciones


B. Aritmética de coma flotante: Consideraciones y limitaciones

Los números de coma flotante se representan dentro del hardware de la computadora como fracciones en base 2 (binarias). Por ejemplo, la fracción decimal

 
0.125

tiene un valor de 1/10 + 2/100 + 5/1000 y, del mismo modo, la fracción binaria

 
0.001

tiene el valor 0/2 + 0/4 + 1/8. Estas dos fracciones tienen idéntico valor. La única diferencia real entre ambas es que la primera está escrita en notación fraccional de base 10 y la segunda en base 2.

Desafortunadamente, la mayoría de las fracciones decimales no se pueden representar como fracciones binarias. Una consecuencia de ello es que, en general, los números de coma flotante decimales que se introducen se almacenan como números sólo aproximados en la máquina.

El problema es más fácil de empezar a entender en base 10. Considera la fracción 1/3. Se puede aproximar mediane una fracción de base 10:

 
0.3

o mejor,

 
0.33

o mejor,

 
0.333

y así sucesivamente. No importa cuántos dígitos se escriban, el resultado nunca será 1/3 exactamente, sino una aproximación cada vez mejor de 1/3.

Del mismo modo, no importa cuantos dígitos de base 2 se usen, el valor decimal 0,1 no se puede representar exactamente como una fracción de base 2. En base 2, 1/10 es la fracción periódica

 
0.0001100110011001100110011001100110011001100110011...

Se puede parar en cualquier número finito de bits y se obtiene una aproximación. Por este motivo se ven cosas como:

 
>>> 0.1 
0.10000000000000001

En la mayoría de las máquinas actuales, esto es lo que se ve si se introduce 0.1 en el símbolo de entrada de Python. Es posible que no, sin embargo, porque el número de bits utilizado por el hardware para almacenar valores de coma flotante puede variar entre máquinas y Python sólo presenta una aproximación decimal de la aproximación binaria almacenada en la máquina. En la mayoría de las máquinas, si Python hubiera de presentar la aproximación binaria de 0.1, ¡tendría que presentar

 
>>> 0.1 
0.1000000000000000055511151231257827021181583404541015625

en su lugar! El modo interactivo de Python (implícitamente) utiliza la función interna repr() para obtener una versión en forma de cadena de todo lo que presenta. Para los valores de coma flotante, repr(float) redondea el valor decimal real a 17 dígitos significativos, resultando

 
0.10000000000000001

repr(float) produce 17 dígitos significativos porque resulta que es suficiente para que eval(repr(x)) == x exactamente para todos los valores de coma flotante finitos x, pero redondear a 16 dígitos no es suficiente para que esto sea cierto.

Hay que darse cuenta de que esto es la naturaleza de la coma flotante binaria, no un bug en Python ni en el código del usuario. Este efecto se observa en todos los lenguajes que dan soporte a la aritmética de coma flotante del hardware (aunque es posible que algunos lenguajes no muestren la diferencia de forma predeterminada o en todos los modos).

La función interna de Python str() produce sólo 12 dígitos significativos, puede que te resulte más útil. No es común que eval(str(x)) reproduzca x, pero el resultado visible puede ser más agradable:

 
>>> print str(0.1) 
0.1

Es importante darse cuenta de que esto es, en sentido estricto, una ilusión: el valor dentro de la máquina no es exactamente 1/10, sólo estás redondeando el valor presentado del verdadero valor almacenado en la máquina.

Se pueden ver otros resultados sorprendentes. Por ejemplo, tras ver

 
>>> 0.1 
0.10000000000000001

se puede ver uno tentado a utilizar la función round() para recortarlo al único dígito decimal esperado. El resultado no cambia:

 
>>> round(0.1, 1) 
0.10000000000000001

El problema es que el valor de coma flotante almacenado para "0,1" ya era la mejor aproximación posible a 1/10 e intentar redondearlo no lo puede mejorar: ya era todo lo bueno posible.

Otra consecuencia es que como 0,1 no es exactamente 1/10, sumar 0,1 consigo mismo 10 veces no da como resultado 1,0, tampoco:

 
>>> suma = 0.0 
>>> for i in range(10): 
...     suma += 0.1 
... 
>>> suma 
0.99999999999999989

La aritmética de coma flotante reserva muchas sorpresas del estilo. El problema con "0,1" se explica con precisión más tarde, en la sección "Error de representación". Consultar en The Perils of Floating Point una recopilación más completa de otras sorpresas habituales.

Como dice cerca del final, ``no hay respuestas fáciles''. Aun así, ¡no hay que temer a la coma flotante! Los errores de las operaciones de coma flotante de Python se heredan del hardware de coma flotante, y en la mayoría de los casos están en el orden de 1 entre 2**53 por operación. Esto basta en la mayoría de los casos, pero hay que mantener presente que no es aritmética decimal y que cada operación en coma flotante puede acumular un nuevo error de redondeo.

Aunque existen casos patológicos, en la mayoría de aritmética de coma flotante informal verás el resultado esperado al final si redondeas la presentación del resultado final al número de cifras esperado. Suele bastar str() para ello. Si se desea un control más fino, consultar la discusión del operador de formato % de Python: los códigos de formato %g, %f y %e proporcionan modos flexibles y sencillos de redondear los resultados de coma flotante para su presentación.


B.1 Error de representación

Esta sección explica el ejemplo del ``0.1'' en detalle y muestra cómo puedes realizar tú mismo un análisis exacto de casos como éste. Se supone una familiaridad básica con la representación de coma flotante binaria.

El error de representación se refiere a que algunas (la mayoría, en realidad) fracciones decimales no se pueden representar exactamente como fracciones binarias (de base 2). Ésta es la principal razón de que Python (o Perl, C C++, Java, Fortran y muchos otros) no suelan presentar el número decimal que esperas:

>>> 0.1
0.10000000000000001

¿A qué se debe esto? 1/10 no es exactamente representable como una fracción binaria. Casi todas las máquinas de hoy en día (noviembre de 2000) usan aritmética de coma flotante de ``doble precisión'' IEEE-754. Los dobles de 753 contienen 53 bits de precisión, así que a la entrada el ordenador se las ve y se las desea para convertir 0,1 a la fracción más cercana de la forma J/2**N donde J es un entero de exactamente 53 bits. Reescribiendo

 1 / 10 ~= J / (2**N)

como

J ~= 2**N / 10

y recordando que J tiene 53 bits (es >= 2**52 pero < 2**53), el mejor valor para N es 56:

>>> 2**52
4503599627370496L
>>> 2L**53
9007199254740992L
>>> 2L**56/10
7205759403792793L

Es decir, 56 es el único valor de N que deja J con exactamente 53 bits. El mejor valor posible para J es, pues, este cociente redondeado:

>>> q, r = divmod(2L**56, 10)
>>> r
6L

Como el resto es mayor que la mitad de 10, la mejor aproximación se obtiene redondeando hacia arriba:

>>> q+1
7205759403792794L

Por consiguiente, la mejor aproximación posible de 1/10 en precision doble 754 es dicho resultado partido por 2**56, o

7205759403792794 / 72057594037927936

Observa que, como hemos redondeado hacia arriba, esto supera ligeramente 1/10; si no hubiéramos redondeado hacia arriba, el cociente hubiera sido ligeramente menor que 1/10. ¡Pero en ningún caso puede ser exactamente 1/10!

Así que el ordenador nunca ``ve'' 1/10: lo que ve es la fracción exacta dada anteriormente, la mejor aproximación doble 754 que puede conseguir:

>>> .1 * 2L**56
7205759403792794.0

Si multiplicamos dicha fracción por 10**30, podemos ver el valor (truncado) de sus 30 cifras decimales más significativas:

>>> 7205759403792794L * 10L**30 / 2L**56
100000000000000005551115123125L

lo que significa que el número exacto almacenado en el ordenador es aproximadamente igual al valor decimal 0,100000000000000005551115123125. Redondeando esto a 17 cifras significativas da el 0.10000000000000001 que presenta Python (bueno, lo hará en cualquier plataforma compatible 754 que realice la mejor conversión posible en entrada y salida, ¡igual en la tuya no!).

Consultar en Acerca de este documento... información para sugerir cambios.