Errores de redondeo en la división de pisos Python.

Sé que los errores de redondeo ocurren en la aritmética de punto flotante, pero alguien puede explicar la razón de esto:

>>> 8.0 / 0.4 # as expected 20.0 >>> floor(8.0 / 0.4) # int works too 20 >>> 8.0 // 0.4 # expecting 20.0 19.0 

Esto sucede tanto en Python 2 como en 3 en x64.

Por lo que veo, esto es un error o una especificación muy tonta de // ya que no veo ninguna razón por la que la última expresión deba evaluar a 19.0 .

¿Por qué no se define simplemente a // b como floor(a / b) ?

EDIT : 8.0 % 0.4 también se evalúa a 0.3999999999999996 . Al menos esto es consecuente desde entonces que 8.0 // 0.4 * 0.4 + 8.0 % 0.4 evalúa a 8.0

EDITAR : Esto no es un duplicado de ¿Se rompen las matemáticas de punto flotante? ya que estoy preguntando por qué esta operación específica está sujeta a errores de redondeo (quizás evitables), y por qué a // b no se define como / igual al floor(a / b)

OBSERVACIÓN : Supongo que la razón más profunda por la que esto no funciona es que la división del piso es discontinua y, por lo tanto, tiene un número de condición infinito que lo convierte en un problema mal planteado. Los números de división de piso y de punto flotante son básicamente incompatibles y nunca debe usar // en flotadores. Solo usa números enteros o fracciones en su lugar.

Como usted y khelwood ya notaron, 0.4 no puede representarse exactamente como un flotador. ¿Por qué? Es la quinta parte ( 4/10 == 2/5 ) que no tiene una representación de fracción binaria finita.

Prueba esto:

 from fractions import Fraction Fraction('8.0') // Fraction('0.4') # or equivalently # Fraction(8, 1) // Fraction(2, 5) # or # Fraction('8/1') // Fraction('2/5') # 20 

sin embargo

 Fraction('8') // Fraction(0.4) # 19 

En este caso, 0.4 se interpreta como un literal flotante (y, por lo tanto, como un número binario de punto flotante) que requiere un redondeo (binario), y solo luego se convierte a la Fraction(3602879701896397, 9007199254740992) número racional Fraction(3602879701896397, 9007199254740992) , que es casi pero no exactamente Fraction(3602879701896397, 9007199254740992) . Luego se ejecuta la división de pisos, y porque

 19 * Fraction(3602879701896397, 9007199254740992) < 8.0 

y

 20 * Fraction(3602879701896397, 9007199254740992) > 8.0 

El resultado es 19, no 20.

Lo mismo probablemente sucede para

 8.0 // 0.4 

Es decir, parece que la división flotante se determina atómicamente (pero en los únicos valores de flotación aproximados de los literales de flotación interpretados).

Entonces, ¿por qué

 floor(8.0 / 0.4) 

dar el resultado "correcto"? Porque allí, dos errores de redondeo se anulan entre sí. Primero 1) se realiza la división, produciendo algo ligeramente más pequeño que 20.0, pero no representable como flotante. Se redondea al flotador más cercano, que es 20.0 . Solo entonces , se realiza la operación de floor , pero ahora actúa exactamente en 20.0 , por lo que ya no se cambia el número.


1) Como señala Kyle Strand, que el resultado exacto se determina y luego se redondea no es lo que realmente sucede en el nivel 2) (código C de CPython o incluso instrucciones de la CPU). Sin embargo, puede ser un modelo útil para determinar el resultado esperado 3) .

2) Sin embargo, en el nivel 4) más bajo, esto podría no estar muy lejos. Algunos conjuntos de chips determinan los resultados de flotación calculando primero un resultado de punto flotante interno más preciso (pero aún no exacto, simplemente tiene algunos dígitos más binarios) y luego redondeando a doble precisión IEEE.

3) "esperado" por la especificación de Python, no necesariamente por nuestra intuición.

4) Bueno, el nivel más bajo por encima de las puertas lógicas. No tenemos que considerar la mecánica cuántica que hace posible que los semiconductores comprendan esto.

@jotasi explicó la verdadera razón detrás de esto.

Sin embargo, si desea evitarlo, puede usar un módulo decimal que fue diseñado básicamente para representar números de coma flotante decimal en contraste con la representación de coma flotante binaria.

Así que en tu caso podrías hacer algo como:

 >>> from decimal import * >>> Decimal('8.0')//Decimal('0.4') Decimal('20') 

Referencia: https://docs.python.org/2/library/decimal.html

Ok después de un poco de investigación he encontrado este problema . Lo que parece estar sucediendo es que, como sugirió @khelwood, 0.4 evalúa internamente a 0.40000000000000002220 , que al dividir 8.0 produce algo ligeramente menor que 20.0 . El operador / luego se redondea al número de punto flotante más cercano, que es 20.0 , pero el operador // trunca inmediatamente el resultado, lo que arroja 19.0 .

Esto debería ser más rápido y supongo que está “cerca del procesador”, pero todavía no es lo que el usuario quiere / está esperando.

Esto se debe a que no hay 0.4 en python (representación finita de punto flotante) en realidad es un flotante como 0.4000000000000001 que hace que el piso de división sea 19.

 >>> floor(8//0.4000000000000001) 19.0 

Pero la división verdadera ( / ) devuelve una aproximación razonable del resultado de la división si los argumentos son flotantes o complejos. Y es por eso que el resultado de 8.0/0.4 es 20. En realidad, depende del tamaño de los argumentos (en C dobles argumentos). ( no redondeando al flotador más cercano )

Lea más sobre los pisos de división de enteros de las pitones por el mismo Guido.

También para obtener información completa sobre los números flotantes, puede leer este artículo https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Para aquellos que tienen interés, la siguiente función es el float_div que realiza la verdadera tarea de división para los números flotantes, en el código fuente de Cpython:

 float_div(PyObject *v, PyObject *w) { double a,b; CONVERT_TO_DOUBLE(v, a); CONVERT_TO_DOUBLE(w, b); if (b == 0.0) { PyErr_SetString(PyExc_ZeroDivisionError, "float division by zero"); return NULL; } PyFPE_START_PROTECT("divide", return 0) a = a / b; PyFPE_END_PROTECT(a) return PyFloat_FromDouble(a); } 

El resultado final se calcularía mediante la función PyFloat_FromDouble :

 PyFloat_FromDouble(double fval) { PyFloatObject *op = free_list; if (op != NULL) { free_list = (PyFloatObject *) Py_TYPE(op); numfree--; } else { op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject)); if (!op) return PyErr_NoMemory(); } /* Inline PyObject_New */ (void)PyObject_INIT(op, &PyFloat_Type); op->ob_fval = fval; return (PyObject *) op; } 

Después de verificar las fonts semioficiales del objeto float en cpython en github ( https://github.com/python/cpython/blob/966b24071af1b320a1c7646d33474eeae057c20f/Objects/floatobject.c ) se puede entender lo que sucede aquí.

Para la división normal, se llama a float_div (línea 560) que convierte internamente el float python s en c- double s, hace la división y luego convierte el double resultante de nuevo en un float python. Si simplemente haces eso con 8.0/0.4 en c, obtienes:

 #include "stdio.h" #include "math.h" int main(){ double vx = 8.0; double wx = 0.4; printf("%lf\n", floor(vx/wx)); printf("%d\n", (int)(floor(vx/wx))); } // gives: // 20.000000 // 20 

Para la división de pisos, algo más sucede. Internamente, se float_floor_div (línea 654), que luego llama a float_divmod , una función que se supone que devuelve una tupla de float de python que contiene la división de pisos, así como el mod / rest, incluso aunque este último se deseche. PyTuple_GET_ITEM(t, 0) . Estos valores se calculan de la siguiente manera (después de la conversión a c- double s):

  1. El rest se calcula utilizando double mod = fmod(numerator, denominator) .
  2. El numerador se reduce por mod para obtener un valor integral cuando luego haces la división.
  3. El resultado para la división de piso se calcula computando efectivamente el floor((numerator - mod) / denominator)
  4. Posteriormente, se realiza la verificación ya mencionada en la respuesta de @ Kasramvd. Pero esto solo ajusta el resultado de (numerator - mod) / denominator al valor integral más cercano.

La razón por la que esto da un resultado diferente es que fmod(8.0, 0.4) debido a la aritmética de punto flotante da 0.4 lugar de 0.0 . Por lo tanto, el resultado que se calcula es realmente el floor((8.0 - 0.4) / 0.4) = 19 y el ajuste (8.0 - 0.4) / 0.4) = 19 al valor integral más cercano no soluciona el error cometido introducido por el “incorrecto” resultado de fmod . Usted puede fácilmente chack que en c también:

 #include "stdio.h" #include "math.h" int main(){ double vx = 8.0; double wx = 0.4; double mod = fmod(vx, wx); printf("%lf\n", mod); double div = (vx-mod)/wx; printf("%lf\n", div); } // gives: // 0.4 // 19.000000 

Supongo que eligieron esta forma de calcular la división de pisos para mantener la validez de (numerator//divisor)*divisor + fmod(numerator, divisor) = numerator (como se menciona en el enlace en la respuesta de @ 0x539), aunque ¡Esto ahora da como resultado un comportamiento inesperado del floor(8.0/0.4) != 8.0//0.4 .