Comportamiento extraño con flotadores y conversión de cuerdas

He escrito esto en la shell de python:

>>> 0.1*0.1 0.010000000000000002 

Esperé que 0.1 * 0.1 no sea 0.01, porque sé que 0.1 en la base 10 es periódico en la base 2.

 >>> len(str(0.1*0.1)) 4 

Esperaba obtener 20 como he visto 20 caracteres arriba. ¿Por qué obtengo 4?

 >>> str(0.1*0.1) '0.01' 

Ok, esto explica por qué len me da 4, pero ¿por qué str devuelve '0.01' ?

 >>> repr(0.1*0.1) '0.010000000000000002' 

¿Por qué se redondea pero no se repr ? (He leído esta respuesta , pero me gustaría saber cómo se decidieron cuándo las rondas son flotantes y cuándo no)

 >>> str(0.01) == str(0.0100000000001) False >>> str(0.01) == str(0.01000000000001) True 

Así que parece ser un problema con la precisión de los flotadores. Pensé que Python usaría flotadores de precisión única IEEE 754. Así que lo he comprobado así:

 #include  #include  // printf union myUnion { uint32_t i; // unsigned integer 32-bit type (on every machine) float f; // a type you want to play with }; int main() { union myUnion testVar; testVar.f = 0.01000000000001f; printf("%f\n", testVar.f); testVar.f = 0.01000000000000002f; printf("%f\n", testVar.f); testVar.f = 0.01f*0.01f; printf("%f\n", testVar.f); } 

Tengo:

 0.010000 0.010000 0.000100 

Python me da:

 >>> 0.01000000000001 0.010000000000009999 >>> 0.01000000000000002 0.010000000000000019 >>> 0.01*0.01 0.0001 

¿Por qué Python me da estos resultados?

(Uso Python 2.6.5. Si conoces las diferencias en las versiones de Python, también me interesarán por ellas).

El requisito crucial en la repr es que debe ser de ida y vuelta; es decir, eval(repr(f)) == f debe dar True en todos los casos.

En Python 2.x (antes de la %.17g 2.7), la repr se realiza realizando una %.17g con formato %.17g y descartando los ceros finales. Esto está garantizado correctamente (para flotadores de 64 bits) por IEEE-754. Desde la versión 2.7 y 3.1, Python utiliza un algoritmo más inteligente que puede encontrar representaciones más cortas en algunos casos donde %.17g proporciona %.17g innecesarios del terminal o nines del terminal. Ver ¿Qué hay de nuevo en 3.1? y número 1580 .

Incluso bajo Python 2.7, repr(0.1 * 0.1) da "0.010000000000000002" . Esto se debe a que 0.1 * 0.1 == 0.01 es False bajo el análisis y la aritmética IEEE-754; es decir, el valor de punto flotante de 64 bits más cercano a 0.1 , cuando se multiplica por sí mismo, produce un valor de punto flotante de 64 bits que no es el valor de punto flotante de 64 bits más cercano a 0.01 :

 >>> 0.1.hex() '0x1.999999999999ap-4' >>> (0.1 * 0.1).hex() '0x1.47ae147ae147cp-7' >>> 0.01.hex() '0x1.47ae147ae147bp-7' ^ 1 ulp difference 

La diferencia entre repr y str (pre-2.7 / 3.1) es que los formatos str con 12 decimales en lugar de 17, que no es triplicable pero produce resultados más legibles en muchos casos.

Puedo confirmar tu comportamiento

 ActivePython 2.6.4.10 (ActiveState Software Inc.) based on Python 2.6.4 (r264:75706, Jan 22 2010, 17:24:21) [MSC v.1500 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> repr(0.1) '0.10000000000000001' >>> repr(0.01) '0.01' 

Ahora, los documentos afirman que en Python <2.7

el valor de repr(1.1) se calculó como format(1.1, '.17g')

Esta es una ligera simplificación.


Tenga en cuenta que todo esto tiene que ver con el código de formato de cadena: en la memoria, todos los flotadores de Python solo se almacenan como C ++ se duplica, por lo que nunca habrá una diferencia entre ellos.

Además, es un poco desagradable trabajar con la cadena de longitud completa para una flotación, incluso si sabes que hay una mejor. De hecho, en los Pythons modernos se usa un nuevo algoritmo para el formato flotante, que elige la representación más corta de una manera inteligente.


Pasé un rato buscando esto en el código fuente, así que incluiré los detalles aquí en caso de que esté interesado. Puedes saltarte esta sección.

En floatobject.c , vemos

 static PyObject * float_repr(PyFloatObject *v) { char buf[100]; format_float(buf, sizeof(buf), v, PREC_REPR); return PyString_FromString(buf); } 

lo que nos lleva a mirar format_float . Omitiendo los casos especiales NaN / inf, es:

 format_float(char *buf, size_t buflen, PyFloatObject *v, int precision) { register char *cp; char format[32]; int i; /* Subroutine for float_repr and float_print. We want float numbers to be recognizable as such, ie, they should contain a decimal point or an exponent. However, %g may print the number as an integer; in such cases, we append ".0" to the string. */ assert(PyFloat_Check(v)); PyOS_snprintf(format, 32, "%%.%ig", precision); PyOS_ascii_formatd(buf, buflen, format, v->ob_fval); cp = buf; if (*cp == '-') cp++; for (; *cp != '\0'; cp++) { /* Any non-digit means it's not an integer; this takes care of NAN and INF as well. */ if (!isdigit(Py_CHARMASK(*cp))) break; } if (*cp == '\0') { *cp++ = '.'; *cp++ = '0'; *cp++ = '\0'; return; }  } 

Podemos ver eso

Entonces, esto primero inicializa algunas variables y comprueba que v es un flotador bien formado. Luego prepara una cadena de formato:

 PyOS_snprintf(format, 32, "%%.%ig", precision); 

Ahora PREC_REPR se define en otras partes en floatobject.c como 17, por lo que esto se calcula como "%.17g" . Ahora llamamos

 PyOS_ascii_formatd(buf, buflen, format, v->ob_fval); 

Con el final del túnel a la vista, buscamos PyOS_ascii_formatd y descubrimos que usa snprintf internamente.

del tutorial de python :

En versiones anteriores a Python 2.7 y Python 3.1, Python redondea este valor a 17 dígitos significativos, dando '0.10000000000000001' . En las versiones actuales, Python muestra un valor basado en la fracción decimal más corta que redondea correctamente al verdadero valor binario, lo que resulta simplemente en '0.1' .