¿Por qué el valor de punto flotante de 4 * 0.1 se ve bien en Python 3 pero 3 * 0.1 no?

158

Sé que la mayoría de los decimales no tienen una representación exacta de coma flotante ( ¿se rompen las matemáticas de coma flotante? ).

Pero no veo por qué 4*0.1se imprime bien como 0.4, pero 3*0.1no lo es, cuando ambos valores tienen representaciones decimales feas:

>>> 3*0.1
0.30000000000000004
>>> 4*0.1
0.4
>>> from decimal import Decimal
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
Aivar
fuente
77
Porque algunos números se pueden representar exactamente y otros no.
Morgan Thrapp
58
@MorganThrapp: no, no lo es. El OP pregunta por la opción de formato de aspecto bastante arbitrario. Ni 0.3 ni 0.4 pueden representarse exactamente en coma flotante binaria.
Betsabé
42
@BartoszKP: Después de haber leído el documento varias veces, no explica por qué Python está mostrando 0.3000000000000000444089209850062616169452667236328125como 0.30000000000000004y 0.40000000000000002220446049250313080847263336181640625como .4a pesar de que parecen tener la misma precisión, y por lo tanto no responde a la pregunta.
Mooing Duck
66
Ver también stackoverflow.com/questions/28935257/… - Estoy un poco irritado porque se cerró como un duplicado, pero este no.
Random832
12
Reabierto, por favor no cierre esto ya que un duplicado de "se ha roto la matemática de coma flotante" .
Antti Haapala

Respuestas:

301

La respuesta simple se 3*0.1 != 0.3debe a un error de cuantización (redondeo) (mientras 4*0.1 == 0.4que multiplicar por una potencia de dos suele ser una operación "exacta").

Puede usar el .hexmétodo en Python para ver la representación interna de un número (básicamente, el valor exacto de punto flotante binario, en lugar de la aproximación de base 10). Esto puede ayudar a explicar lo que sucede debajo del capó.

>>> (0.1).hex()
'0x1.999999999999ap-4'
>>> (0.3).hex()
'0x1.3333333333333p-2'
>>> (0.1*3).hex()
'0x1.3333333333334p-2'
>>> (0.4).hex()
'0x1.999999999999ap-2'
>>> (0.1*4).hex()
'0x1.999999999999ap-2'

0.1 es 0x1.999999999999a veces 2 ^ -4. La "a" al final significa el dígito 10; en otras palabras, 0.1 en coma flotante binaria es muy ligeramente mayor que el valor "exacto" de 0.1 (porque el 0x0.99 final se redondea a 0x0.a). Cuando multiplica esto por 4, una potencia de dos, el exponente se mueve hacia arriba (de 2 ^ -4 a 2 ^ -2) pero el número no cambia, por lo tanto 4*0.1 == 0.4.

Sin embargo, cuando multiplica por 3, la pequeña pequeña diferencia entre 0x0.99 y 0x0.a0 (0x0.07) aumenta en un error de 0x0.15, que se muestra como un error de un dígito en la última posición. Esto hace que 0.1 * 3 sea un poco más grande que el valor redondeado de 0.3.

El flotador de Python 3 represtá diseñado para ser de ida y vuelta , es decir, el valor que se muestra debe ser exactamente convertible en el valor original. Por lo tanto, no se puede mostrar 0.3y 0.1*3exactamente de la misma manera, o las dos diferentes números terminaría el mismo después de ida y vuelta de. En consecuencia, el reprmotor de Python 3 elige mostrar uno con un ligero error aparente.

nneonneo
fuente
25
Esta es una respuesta increíblemente completa, gracias. (En particular, gracias por mostrar .hex(); no sabía que existía).
NPE
21
@supercat: Python intenta encontrar la cadena más corta que se redondea al valor deseado , sea lo que sea. Obviamente, el valor evaluado debe estar dentro de 0.5ulp (o se redondearía a otra cosa), pero puede requerir más dígitos en casos ambiguos. El código es muy complejo, pero si quieres echar un vistazo: hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345
nneonneo
2
@supercat: siempre la cadena más corta dentro de 0.5 ulp. ( Estrictamente dentro si estamos mirando un flotador con LSB impar; es decir, la cadena más corta que hace que funcione con lazos redondos a pares). Cualquier excepción a esto es un error, y debe ser reportado.
Mark Dickinson
77
@MarkRansom Seguramente usaron algo más que eporque ya es un dígito hexadecimal. Tal vez ppor el poder en lugar de exponente .
Bergi
11
@Bergi: El uso de pen este contexto se remonta (al menos) a C99, y también aparece en IEEE 754 y en varios otros idiomas (incluido Java). Cuando float.hexy float.fromhexfueron implementados (por mí :-), Python simplemente estaba copiando lo que para entonces era una práctica establecida. No sé si la intención era 'p' para "Power", pero parece una buena manera de pensarlo.
Mark Dickinson
75

repr(y stren Python 3) sacará tantos dígitos como sea necesario para que el valor no sea ambiguo. En este caso, el resultado de la multiplicación 3*0.1no es el valor más cercano a 0.3 (0x1.3333333333333p-2 en hexadecimal), en realidad es un LSB más alto (0x1.3333333333334p-2), por lo que necesita más dígitos para distinguirlo de 0.3.

Por otra parte, la multiplicación 4*0.1 hace obtener el valor más cercano a 0,4 (0x1.999999999999ap-2 en hexadecimal), por lo que no necesita ningún dígitos adicionales.

Puede verificar esto con bastante facilidad:

>>> 3*0.1 == 0.3
False
>>> 4*0.1 == 0.4
True

Utilicé la notación hexadecimal anterior porque es agradable y compacta y muestra la diferencia de bits entre los dos valores. Puede hacerlo usted mismo usando, por ejemplo (3*0.1).hex(). Si prefieres verlos en toda su gloria decimal, aquí tienes:

>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
>>> Decimal(0.4)
Decimal('0.40000000000000002220446049250313080847263336181640625')
Mark Ransom
fuente
2
(+1) Buena respuesta, gracias. ¿Crees que valdría la pena ilustrar el punto "no el valor más cercano" al incluir el resultado de 3*0.1 == 0.3y 4*0.1 == 0.4?
NPE
@ NPE Debería haberlo hecho desde el principio, gracias por la sugerencia.
Mark Ransom
Me pregunto si valdría la pena señalar los valores decimales precisos de los "dobles" más cercanos a 0.1, 0.3 y 0.4, ya que mucha gente no puede leer el hexadecimal de punto flotante.
supercat
@ Supercat haces un buen punto. Poner esos dobles súper grandes en el texto sería una distracción, pero pensé en una forma de agregarlos.
Mark Ransom
25

Aquí hay una conclusión simplificada de otras respuestas.

Si marca un flotante en la línea de comando de Python o lo imprime, pasa por la función reprque crea su representación de cadena.

A partir de la versión 3.2, Python stry reprutilizar un esquema de redondeo compleja, que prefiere decimales agradables de aspecto, si es posible, pero utiliza más dígitos cuando sea necesario para biyectiva de garantía (uno a uno) mapeo entre los flotadores y sus representaciones de cadena.

Este esquema garantiza que el valor de se repr(float(s))vea bien para decimales simples, incluso si no se pueden representar con precisión como flotantes (por ejemplo, cuándo s = "0.1").

Al mismo tiempo, garantiza que se float(repr(x)) == xmantenga para cada flotadorx

Aivar
fuente
2
Su respuesta es precisa para las versiones de Python> = 3.2, donde stry reprson idénticas para las carrozas. Para Python 2.7, reprtiene las propiedades que identifica, pero stres mucho más simple: simplemente calcula 12 dígitos significativos y produce una cadena de salida basada en ellos. Para Python <= 2,6, tanto repry strse basan en un número fijo de dígitos significativos (17 para repr, 12 para str). (Y a nadie le importa Python 3.0 o Python 3.1 :-)
Mark Dickinson
Gracias @MarkDickinson! Incluí tu comentario en la respuesta.
Aivar
2
Tenga en cuenta que el redondeo de Shell viene de represte modo el comportamiento de Python 2.7 sería idéntico ...
Antti Haapala
5

No es realmente específico para la implementación de Python, pero debería aplicarse a cualquier función de cadena flotante a decimal.

Un número de coma flotante es esencialmente un número binario, pero en notación científica con un límite fijo de cifras significativas.

El inverso de cualquier número que tenga un factor de número primo que no se comparta con la base siempre dará como resultado una representación de punto de punto recurrente. Por ejemplo, 1/7 tiene un factor primo, 7, que no se comparte con 10, y por lo tanto tiene una representación decimal recurrente, y lo mismo es cierto para 1/10 con los factores primos 2 y 5, este último no se comparte con 2 ; Esto significa que 0.1 no puede representarse exactamente por un número finito de bits después del punto de punto.

Como 0.1 no tiene una representación exacta, una función que convierte la aproximación a una cadena de punto decimal generalmente intentará aproximar ciertos valores para que no obtengan resultados no intuitivos como 0.1000000000004121.

Dado que el punto flotante está en notación científica, cualquier multiplicación por una potencia de la base solo afecta a la parte exponente del número. Por ejemplo 1.231e + 2 * 100 = 1.231e + 4 para notación decimal, y de la misma manera, 1.00101010e11 * 100 = 1.00101010e101 en notación binaria. Si multiplico por un no poder de la base, los dígitos significativos también se verán afectados. Por ejemplo 1.2e1 * 3 = 3.6e1

Dependiendo del algoritmo utilizado, puede tratar de adivinar decimales comunes basados ​​solo en cifras significativas. Tanto 0.1 como 0.4 tienen las mismas cifras significativas en binario, porque sus flotadores son esencialmente truncamientos de (8/5) (2 ^ -4) y (8/5) (2 ^ -6) respectivamente. Si el algoritmo identifica el patrón sigfig 8/5 como el decimal 1.6, entonces funcionará en 0.1, 0.2, 0.4, 0.8, etc. También puede tener patrones sigfig mágicos para otras combinaciones, como el flotador 3 dividido por el flotador 10 y otros patrones mágicos estadísticamente propensos a formarse por división por 10.

En el caso de 3 * 0.1, las últimas cifras significativas probablemente serán diferentes de dividir un flotador 3 por el flotador 10, haciendo que el algoritmo no reconozca el número mágico para la constante 0.3 dependiendo de su tolerancia a la pérdida de precisión.

Editar: https://docs.python.org/3.1/tutorial/floatingpoint.html

Curiosamente, hay muchos números decimales diferentes que comparten la misma fracción binaria aproximada más cercana. Por ejemplo, los números 0.1 y 0.10000000000000001 y 0.1000000000000000055511151231257827021181583404541015625 se aproximan a 3602879701896397/2 ** 55. Dado que todos estos valores decimales comparten la misma aproximación, cualquiera de ellos podría mostrarse mientras se conserva la evaluación invariante (repr) (repr) ) == x.

No hay tolerancia para la pérdida de precisión, si float x (0.3) no es exactamente igual a float y (0.1 * 3), entonces repr (x) no es exactamente igual a repr (y).

AkariAkaori
fuente
44
Esto realmente no agrega mucho a las respuestas existentes.
Antti Haapala
1
"Dependiendo del algoritmo utilizado, puede tratar de adivinar decimales comunes basados ​​solo en cifras significativas". <- Esto parece pura especulación. Otras respuestas han descrito lo que realmente hace Python .
Mark Dickinson