rounding errors in Python floor division

前端 未结 5 1052
挽巷
挽巷 2020-12-04 21:29

I know rounding errors happen in floating point arithmetic but can somebody explain the reason for this one:

>>> 8.0 / 0.4  # as expected
20.0
>&         


        
5条回答
  •  南笙
    南笙 (楼主)
    2020-12-04 22:29

    After checking the semi-official sources of the float object in cpython on github (https://github.com/python/cpython/blob/966b24071af1b320a1c7646d33474eeae057c20f/Objects/floatobject.c) one can understand what happens here.

    For normal division float_div is called (line 560) which internally converts the python floats to c-doubles, does the division and then converts the resulting double back to a python float. If you simply do that with 8.0/0.4 in c you get:

    #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
    

    For the floor division, something else happens. Internally, float_floor_div (line 654) gets called, which then calls float_divmod, a function that is supposed to return a tuple of python floats containing the floored division, as well as the mod/remainder, even though the latter is just thrown away by PyTuple_GET_ITEM(t, 0). These values are computed the following way (After conversion to c-doubles):

    1. The remainder is computed by using double mod = fmod(numerator, denominator).
    2. The numerator is reduced by mod to get a integral value when you then do the division.
    3. The result for the floored division is calculated by effectively computing floor((numerator - mod) / denominator)
    4. Afterwards, the check already mentioned in @Kasramvd's answer is done. But this only snaps the result of (numerator - mod) / denominator to the nearest integral value.

    The reason why this gives a different result is, that fmod(8.0, 0.4) due to floating-point arithmetic gives 0.4 instead of 0.0. Therefore, the result that is computed is actually floor((8.0 - 0.4) / 0.4) = 19 and snapping (8.0 - 0.4) / 0.4) = 19 to the nearest integral value does not fix the error made introduced by the "wrong" result of fmod. You can easily chack that in c as well:

    #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
    

    I would guess, that they chose this way of computing the floored division to keep the validity of (numerator//divisor)*divisor + fmod(numerator, divisor) = numerator (as mentioned in the link in @0x539's answer), even though this now results in a somewhat unexpected behavior of floor(8.0/0.4) != 8.0//0.4.

提交回复
热议问题