Issue 36028: Integer Division discrepancy with float (original) (raw)

Issue36028

Created on 2019-02-18 20:15 by Au Vo, last changed 2022-04-11 14:59 by admin. This issue is now closed.

Files
File name Uploaded Description Edit
Screen Shot 2019-02-18 at 9.07.37 PM.png Au Vo,2019-02-19 05:14 Python 3 screenshot from trinket.io
Screen Shot 2019-02-18 at 9.08.19 PM.png Au Vo,2019-02-19 05:17 Python 2 screenshot from trinket.io
Messages (14)
msg335863 - (view) Author: Au Vo (Au Vo) Date: 2019-02-18 20:15
In Python3, there is a discrepancy of integer division with decimal. Considering these two examples: 4/ .4 ==10.0. But 4//.4== 9.0 Furthermore: 2//.2 == 9.0 3//.3 ==10.0 5//.5 ==10.0 6//.6 ==10.0 All answers should be 10.0? The problem is in Python3 and not in Python2. Python2 produces 10.0 as the answers for all of the above calculation. Is it a rounding out issue?
msg335866 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2019-02-18 20:35
I get the same results in both Python 2 and Python 3. Python 2.7.15 (v2.7.15:ca079a3ea3, Apr 29 2018, 20:59:26) [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] on darwin Type "copyright", "credits" or "license()" for more information. >>> 4 / 0.4 10.0 >>> 4 // 0.4 9.0 >>> 4 % 0.4 0.3999999999999998 >>> divmod(4, 0.4) (9.0, 0.3999999999999998) Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 24 2018, 02:44:43) [Clang 6.0 (clang-600.0.57)] on darwin Type "help", "copyright", "credits" or "license()" for more information. >>> 4 / 0.4 10.0 >>> 4 // 0.4 9.0 >>> 4 % 0.4 0.3999999999999998 >>> divmod(4, 0.4) (9.0, 0.3999999999999998)
msg335867 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2019-02-18 21:01
I'm thinking this due to having two different algorithms in play, one of which has multiple intermediate roundings. * For starters, 0.4 is not exactly representable. It is stored as the binary fraction 3602879701896397 / 9007199254740992 represented in hex as 0x1.999999999999ap-2 * The / true division operator is implemented in Objects/floatobject.c:float_div() with a single, straight division of C doubles: "a = a / b;" That is giving 10.0 when rounded. * The // floor division operator is implemented in Objects/floatobject.c:float_floor_div() which in turn calls float_divmod(). The latter has a number of steps that can each have a round-off and can make adjustments to preserve sign logic invariants: mod = fmod(vx, wx); /* fmod is typically exact, so vx-mod is *mathematically* an exact multiple of wx. But this is fp arithmetic, and fp vx - mod is an approximation; the result is that div may not be an exact integral value after the division, although it will always be very close to one. */ div = (vx - mod) / wx; if (mod) { /* ensure the remainder has the same sign as the denominator */ if ((wx < 0) != (mod < 0)) { mod += wx; div -= 1.0; } } else { /* the remainder is zero, and in the presence of signed zeroes fmod returns different results across platforms; ensure it has the same sign as the denominator. */ mod = copysign(0.0, wx); } /* snap quotient to nearest integral value */ if (div) { floordiv = floor(div); if (div - floordiv > 0.5) floordiv += 1.0; } else { /* div is zero - get the same sign as the true quotient */ floordiv = copysign(0.0, vx / wx); /* zero w/ sign of vx/wx */ } I don't see how to fix the discrepancy without having float_div() depend on the much slower logic in float_divmod().
msg335872 - (view) Author: Tim Peters (tim.peters) * (Python committer) Date: 2019-02-18 22:46
"Multiple roundings" in the float code is a red herring - that's just implementation details trying to cheaply get the same effect as computing with infinite precision. Here with actual unbounded precision: >>> from fractions import Fraction >>> y = Fraction(0.4) >>> y Fraction(3602879701896397, 9007199254740992) >>> q = 4 / y >>> q Fraction(36028797018963968, 3602879701896397) >>> int(q) 9 >>> 4 - 9 * y Fraction(3602879701896395, 9007199254740992) >>> float(_) 0.3999999999999998 >>> So exactly the same results as divmod(4, 0.4) returned. The underlying problem here is that the infinitely precise result of 4.0 / 0.4 is NOT an integer, in turn stemming from that the float 0.4 is not four tenths. So I recommend to close this as not-a-bug, but I'm not doing that now because I want clarification on what the OP meant by saying the results differ between Pythons 2 and 3. I see no differences here between Pythons 2.7.11 and 3.7.2 on 64-bit Windows (not in 4.0 vs 0.4, or in any of the other cases the OP mentioned).
msg335874 - (view) Author: Steven D'Aprano (steven.daprano) * (Python committer) Date: 2019-02-18 23:29
Changing the title from referring to "decimal" to "float", since this has nothing to do with the decimal module or Decimal type. Like Raymond and Tim, I too cannot reproduce the claimed difference in behaviour between Python 2.7 and 3.x. Au Vo, there are many resources on the web explaining why floats such as 0.4 do not equal exactly four tenths. One of the best (but not the easiest to understand) is What Every Computer Scientist Should Know About Floating-Point Arithmetic https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html A more accessible (to me at least) resource is Bruce Dawson's blog: https://randomascii.wordpress.com/category/floating-point/page/1/ although it is written from the perspective of a C programmer. There's also a Python FAQ about it: https://docs.python.org/3/faq/design.html#why-are-floating-point-calculations-so-inaccurate
msg335878 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2019-02-19 01:31
The open question in my mind is which is the least surprising definition of a//b. Should it be math.floor(a/b) or divmod(a,b)[0]? The advantage of the former is that floor(a/b) is arguably the definition of floor division. The advantage of the latter is that a//b, a%b, and divmod(a,b) are consistent with one another. FWIW, it looks like NumPy and PyPy made the same design choice as CPython: >>> a = numpy.array([4.0, 4.0, 4.0], dtype=numpy.float64) >>> b = numpy.array([0.4, 0.5, 0.6], dtype=numpy.float64) >>> a / b array([10. , 8. , 6.66666667]) >>> a // b array([9., 8., 6.]) $ pypy3 Python 3.5.3 (fdd60ed87e941677e8ea11acf9f1819466521bf2, Apr 26 2018, 01:25:35) [PyPy 6.0.0 with GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.1)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>>> 4.0 / 0.4 10.0 >>>> 4.0 // 0.4 9.0
msg335882 - (view) Author: Tim Peters (tim.peters) * (Python committer) Date: 2019-02-19 03:53
Thanks, Steven! I'll go on to suggest that the best intro to these issues for Python programmers is in Python's own tutorial: https://docs.python.org/3/tutorial/floatingpoint.html Raymond, `divmod(a, b)[0]` IS the mathematical `floor(a/b)`, where the latter is computed (as if) with infinite precision. Rounding `a/b` to a float _before_ taking the floor is quite different, and was never seriously considered for Python. For one thing, the result is ill-defined unless you control the rounding mode used by HW float division (4.0 / 0.4 == 10.0 only if the rounding mode is to-nearest/even or to-plus-infinity; it's 1 ULP less if to-minus-infinity or to-zero; then taking the floor gives 10 in the former cases but 9 in the latter). It makes scant sense for the result of // to depend on rounding mode: the very name - "floor division" - implies rounding is forced. More fundamentally, a = (a // b)*b + (a % b) is the invariant Guido was most keen to preserve. In any conforming C implementation (by the standard today, but by best practice at the time), fmod(4.0, 0.4) == 0.3999999999999998 on a box with 754 doubles, and Python had no interest in building its own idea of "mod" entirely from scratch. Given that, 4 // 0.4 has to return 9. That said, C's fmod makes more sense for floats than Python's % (which latter makes more sense for integers than C's integer %), because fmod is always exact. Python's float % cannot be, because it retains the sign of the modulus: in, e.g., 1 % -1e100, there is no mathematical integer `n` such that the mathematical 1 + n*-1e100 is both negative and representable as a float. >>> 1 % -1e100 -1e+100 # negative, but not exactly 1 + 1*-1e100 >>> math.fmod(1, -1e100) 1.0 # is exactly 1 + 0*-1e100, but not negative In any case, none of this is going to change after 30 years ;-) For Python 3 I had thought Guido agreed to change a % b for floats to return an exact result (exactly equal to the mathematical a - q*b for some mathematical integer q) such that abs(a % b) <= abs(b)/2, which is most useful most often for floats. But that didn't happen.
msg335884 - (view) Author: Au Vo (Au Vo) Date: 2019-02-19 05:14
Thanks for the quick turnaround. I have done a little bit more thorough investigation and it does indeed the floating point that causes the behavior. The screenshot attached is done via Python3 from trinket.io. I will submit a screenshot of the Python 2 screenshot from trinket.io for comparison. Even though floating point is the underlying concept, Python 2 and 3 do not converge in the result. On the side note, The behavior could be more easily expressed as follows: 2// .2 == 9.0 2000000000/.2 == 9999999999.0
msg335885 - (view) Author: Au Vo (Au Vo) Date: 2019-02-19 05:17
Here is the Python2 screenshot from trinket.io. There are discrepancies with 1,2,4,8,9, with 9 is given in Python2 and 10 is given in Python3. Again, sorry that Trinket.io does not specify which version of Python3 it implements.
msg335898 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2019-02-19 08:16
[Au Vo] > Python2 produces 10.0 as the answers for all of the above calculation. Can you double check this, and confirm which Python version and operating system you're using? Like others, I'm not seeing any difference between Python 2 and Python 3, and as far as I'm aware the implementation of `//` for floats hasn't changed between Python 2 and Python 3. There's no behavioural bug here: all operations are producing perfectly correctly rounded results, which is about as good as one could possibly hope for. And as Tim says: > In any case, none of this is going to change after 30 years ;-) It's surprising, sure, but it's correct, and I can't imagine any fudge that would make the two results match without introducing a horde of other weird corner cases. [Tim] > For Python 3 I had thought Guido agreed to change a % b for floats to > return an exact result (exactly equal to the mathematical a - q*b for > some mathematical integer q) such that abs(a % b) <= abs(b)/2, which > is most useful most often for floats. But that didn't happen. If it's any consolation, we do at least have `math.remainder` in Python 3.7, which does exactly this. Closing this issue.
msg335899 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2019-02-19 08:24
> Can you double check this, and confirm which Python version and operating system you're using? Sorry; I missed the screenshots. It looks as though you labelled those the wrong way around, BTW: I think "Screen Shot 2019-02-18 at 9.07.37 PM.png" is the Python 2 one, and it looks as though Trinket does indeed give a result of 10 for 1 // 0.1. Unfortunately, there's little information about how Trinket is implemented. Is it CPython-based?
msg335902 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2019-02-19 08:35
> Unfortunately, there's little information about how Trinket is implemented. Is it CPython-based? From a little playing around, Trinket only provides a subset of Python (one might say "batteries not included"), and it's not clear what its basis is. None of `sys.version_info`, `sys.platform`, `sys.float_info`, `sys.float_repr_style`, `int.bit_length` or `math.fmod` was available in my Python 2 tests. 1 % 0.1 gives 0.1, but 1 // 0.1 gives 10, so the invariant that Tim mentions is broken on Trinket. @Au Vo: it looks as though you should take this up with the Trinket developers. It looks like a problem with their Python-subset implementation.
msg335907 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2019-02-19 08:54
From a little more digging, it seems that Trinket's Python 2 is based on Skulpt (so is a JavaScript implementation of a subset of Python), while the Python 3 "trinket" connects to an Ubuntu Linux server running CPython 3.6.6.
msg335982 - (view) Author: Au Vo (Au Vo) Date: 2019-02-19 17:08
thanks for the clarification. You have been a great help. Love Python and our supportive community!
History
Date User Action Args
2022-04-11 14:59:11 admin set github: 80209
2021-02-07 20:33:15 ba5367125 set nosy: + ba5367125
2019-02-19 17:08:49 Au Vo set messages: +
2019-02-19 08:54:47 mark.dickinson set messages: +
2019-02-19 08:49:31 mark.dickinson set resolution: not a bug -> third party
2019-02-19 08:35:53 mark.dickinson set messages: +
2019-02-19 08:24:05 mark.dickinson set messages: +
2019-02-19 08:16:01 mark.dickinson set status: open -> closedresolution: not a bugmessages: + stage: resolved
2019-02-19 05:17:35 Au Vo set files: + Screen Shot 2019-02-18 at 9.08.19 PM.pngmessages: + versions: + Python 2.7, - Python 3.8
2019-02-19 05:14:49 Au Vo set files: + Screen Shot 2019-02-18 at 9.07.37 PM.pngmessages: +
2019-02-19 03:53:33 tim.peters set messages: +
2019-02-19 01:31:39 rhettinger set messages: +
2019-02-18 23:29:07 steven.daprano set nosy: + steven.dapranomessages: + title: Integer Division discrepancy with decimal -> Integer Division discrepancy with float
2019-02-18 22:46:41 tim.peters set messages: +
2019-02-18 21:01:16 rhettinger set nosy: + tim.peters, mark.dickinson, skrahmessages: +
2019-02-18 20:45:41 remi.lapeyre set nosy: + remi.lapeyre
2019-02-18 20:35:46 rhettinger set nosy: + rhettingermessages: +
2019-02-18 20:15:14 Au Vo create