This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: 0.0 and -0.0 identified, with surprising results
Type: behavior Stage: needs patch
Components: Interpreter Core Versions: Python 3.1, Python 3.2, Python 2.7, Python 2.6
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: mark.dickinson Nosy List: aleax, christian.heimes, ggenellina, loewis, mancausoft, mark.dickinson, rhettinger
Priority: normal Keywords: patch

Created on 2007-03-11 17:16 by mark.dickinson, last changed 2022-04-11 14:56 by admin. This issue is now closed.

Files
File name Uploaded Description Edit
plus_minus_0j.patch mark.dickinson, 2008-01-21 22:58
issue1678380.patch mark.dickinson, 2009-10-24 15:29
Messages (24)
msg31479 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2007-03-11 17:16
The identification of -0.0 and 0.0 in scripts leads to some surprising
results.  In particular, code that behaves one way in the interpreter can behave differently in a script.  For example:

Python 2.6a0 (trunk:54183M, Mar  6 2007, 20:16:00) 
[GCC 4.0.1 (Apple Computer, Inc. build 5367)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from math import atan2;
>>> x = -0.
>>> y = 0.
>>> print atan2(y, -1.)
3.14159265359

But:

>>> exec("from math import atan2; x = -0.; y = 0.; print atan2(y, -1.)")
-3.14159265359

A simpler example:

>>> x, y = -0., 0.
>>> x, y
(-0.0, -0.0)
>>> id(x) == id(y)
True

But:

>>> x = -0.
>>> y = 0.
>>> x, y
(-0.0, 0.0)

This occurs both on SuSE Linux 9.3/i686 and on OS X 10.4.8/PowerPC.
msg31480 - (view) Author: Martin v. Löwis (loewis) * (Python committer) Date: 2007-03-11 18:35
This is not a bug, at least not one that will be fixed. Details of the floating-point can vary across platforms, and they may behave in suprising ways in various contexts. Users shouldn't rely on Python differentiating between -0 and +0.
msg31481 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2007-03-11 19:21
May I beg for reconsideration.
Is the following really considered acceptable?

>>> x = -0.; atan2(0., -1)
-3.1415926535897931
>>> x = -(0.); atan2(0., -1)
3.1415926535897931
>>> atan2(0., -1)
3.1415926535897931

A single x = -0. at the start of a script can have
side effects several hundred lines later, even when
the variable x is never referred to again.  I guess
the advice should be:  "To avoid surprises, -0. should
never appear in any script."
msg31482 - (view) Author: Gabriel Genellina (ggenellina) Date: 2007-03-11 20:10
It appears to be a problem in the way compile.c handles literals.
See http://mail.python.org/pipermail/python-list/2007-March/430302.html
msg31483 - (view) Author: Martin v. Löwis (loewis) * (Python committer) Date: 2007-03-11 22:27
marketdickinson, you should ask this question (is this really acceptable) on python-dev. I find it perfectly acceptable. No program should rely on -0 and +0 being two different things (and thus also not relying on atan2 giving two different results).
msg31484 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2007-03-12 01:15
I expressed myself badly.  I apologise.  This really isn't about +0. and -0. 
being different, or not.  I'm perfectly comfortable with the idea that +0. 
and -0. may or may not be distinguishable on any given platform.

The surprise is the other way around: two *identical* calls to atan(0., -1.)
(or to repr(0.) for that matter) give *different* results, depending solely 
on whether a -0. literal has appeared earlier on in the code unit being compiled.

So if the first float zero literal encountered in a source file just happens to be a
-0. rather than a 0., the meaning of str(0.) later on suddenly becomes "-0.0"
rather than "0.0".  I'd like to be able to rely on str(0.) meaning "0.0" without
having to worry about whether there might be a -0. literal appearing in some
faraway and otherwise completely irrelevant portion of the file.
msg31485 - (view) Author: Alex Martelli (aleax) * (Python committer) Date: 2007-03-12 01:51
Also see my patch 1678380 which fixes this bug (and checks for it in a new unittest).
msg31486 - (view) Author: Alex Martelli (aleax) * (Python committer) Date: 2007-03-12 01:54
Oops,sorry, I meant patch 1678668 (copy-and-pasted the wrong ID:-).

Alex
msg31487 - (view) Author: Martin v. Löwis (loewis) * (Python committer) Date: 2007-03-12 09:04
I also expressed myself badly. I not only meant that programs should not rely on +0 and -0 being different things across platforms, but that they should also not rely on them being either always different or always the same in a single program. If Python would randomly chose to interpret +0 as -0, or would do so for every third occurence, I still couldn't see a problem.
msg31488 - (view) Author: Alex Martelli (aleax) * (Python committer) Date: 2007-03-12 14:57
and yet, peephole.c does specialcase -0 (with a comment and all!-) avoiding constant-folding optimization for it -- making things work fine in Python 2.4.x for high enough x -- it's just that peephole.c's efforts are defeated by a similar optimization applied without specialcase checking in ast.c in Python 2.5.  This is inconsistent: it makes no sense to take SOME of the precautions needed to avoid an issue but not ALL of them, since this makes the issue appear anyway, so those precautions that ARE taken are there for no purpose (except a miniscule slowdown and enlargement of the interpreter:-).  Either we save those few lines of code in peephole.c or add the few lines to ast.c that I suggest in my patch 1678668 -- the current situation makes no sense.
msg31489 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2007-04-02 19:02
The distinction between +0.0 and -0.0 was important to Tim for get the branch cuts to work correctly.  That was the reason for the special-casing in earlier versions in pre-ast versions of compile.c and in the peephole optimizer.

Alex's patch looks correct.  It should go into Py2.5.1.
msg57339 - (view) Author: Christian Heimes (christian.heimes) * (Python committer) Date: 2007-11-09 22:24
It's fixed in 2.6 but still broken in 2.5.
msg61455 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2008-01-21 21:58
This was fixed in the trunk in revision 57284.  I've backported the fix to Python 2.5.2 in revision 
60183.

Leaving this open because there's still a problem for complex numbers, though I guess this is less 
likely to bite people:

Python 2.6a0 (trunk:60158M, Jan 21 2008, 16:21:41) 
[GCC 4.0.1 (Apple Computer, Inc. build 5370)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 0j
0j
>>> -0j
-0j
>>> [0j, -0j]
[0j, 0j]
msg61462 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2008-01-21 22:58
Here's a patch, against the trunk, that imitates Alex's fix for the complex case.
msg61935 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2008-01-31 22:19
Bug identifying 0j and -0j fixed for Python 2.6 in revision 60483.

Martin, can this be backported to 2.5.2?  I'll assume not, unless I hear
otherwise from you.
msg62370 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2008-02-13 22:26
Closing.
msg94418 - (view) Author: Mancausoft (mancausoft) Date: 2009-10-24 14:13
This bug is still present on arm. 

Python 2.6.3 

cs-e9302# cat ../prova.py
import  math
print math.atan2(0., -0.)
print (math.copysign(4., -0.), -4.0)
print math.atan2(0., -0.)
print (math.copysign(4., -0.), -4.0)
print math.atan2(0., -0.)

cs-e9302# cat ../prova1.py
import  math
print (math.copysign(4., -0.), -4.0)
print math.atan2(0., -0.)
print (math.copysign(4., -0.), -4.0)
print math.atan2(0., -0.)

cs-e9302# ./python ../prova1.py
(-4.0, -4.0)
-3.14159265359
(-4.0, -4.0)
-3.14159265359
cs-e9302# ./python ../prova.py
0.0
(4.0, -4.0)
0.0
(4.0, -4.0)
0.0




>>> from math import atan2
>>> x = -0.
>>> y = 0.
>>> print atan2(y, -1.)
3.14159265359
>>> exec("from math import atan2; x = -0.; y = 0.; print atan2(y,
-1.)")
-3.14159265359
>>> x = -0.; atan2(0., -1)
-3.1415926535897931
>>> x = 0.; atan2(0., -1)
3.1415926535897931

======================================================================
FAIL: testAtan2 (__main__.MathTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "Lib/test/test_math.py", line 131, in testAtan2
    self.ftest('atan2(0., -0.)', math.atan2(0., -0.), math.pi)
  File "Lib/test/test_math.py", line 57, in ftest
    (name, value, expected))
AssertionError: atan2(0., -0.) returned 0.0, expected
3.1415926535897931

======================================================================
FAIL: testCopysign (__main__.MathTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "Lib/test/test_math.py", line 806, in testCopysign
    self.assertEqual(math.copysign(4., -0.), -4.0)
AssertionError: 4.0 != -4.0

----------------------------------------------------------------------
msg94419 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2009-10-24 14:19
Mancausoft:  is this little-endian, OABI?

If so, then I think I know  what the problem is:  the disambiguation code 
in compile.c looks at the first and last bytes of the double to 
distinguish 0.0 and -0.0;  for mixed-endian (aka little-endian, swapped 
words) doubles this will fail.

The solution is to use copysign instead.
msg94422 - (view) Author: Mancausoft (mancausoft) Date: 2009-10-24 15:14
Mark Dickinson <report@bugs.python.org> scrisse:

> Mancausoft:  is this little-endian, OABI?

Mixed endian

> If so, then I think I know  what the problem is:  the disambiguation
> code in compile.c looks at the first and last bytes of the double to 
> distinguish 0.0 and -0.0;  for mixed-endian (aka little-endian,
> swapped words) doubles this will fail.
> 
> The solution is to use copysign instead.

I try: *p==0 && p[sizeof(double)-1]==0 && p[(sizeof(double)-1)/2]==0;

and now the test_math result is:

Ran 39 tests in 21.323s

OK        

It's a safe patch?

Mancausoft
msg94423 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2009-10-24 15:21
> I try: *p==0 && p[sizeof(double)-1]==0 && p[(sizeof(double)-1)/2]==0;

Sure, that should work.  It would seem cleaner and safer to use copysign, 
though: that way, things will still work when some other byte layout comes 
along, or when some version of Python starts using 128-bit IEEE 754 
doubles instead of 64-bit, or ...

Reopening:  I've been meaning to fix these checks to use copysign for a 
while now, anyway.
msg94424 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2009-10-24 15:29
Here's a patch for floats.  Mancausoft, could you give this a try and let 
me know whether it fixes the issue?

The same fix would also need to be applied for complex numbers.
msg94426 - (view) Author: Mancausoft (mancausoft) Date: 2009-10-24 15:45
Mark Dickinson <report@bugs.python.org> scrisse:

> 
> Mark Dickinson <dickinsm@gmail.com> added the comment:
> 
> Here's a patch for floats.  Mancausoft, could you give this a try and
> let me know whether it fixes the issue?

it works.

test_math

Ran 39 tests in 23.561s                                               

OK

test_float
Ran 19 tests in 275.241s

OK

Mancausoft
msg94427 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2009-10-24 16:02
Thanks for testing!  I'll apply the fix once the 2.6 branch is unfrozen.
msg95794 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2009-11-28 16:40
Applied (along with a corresponding fix for the complex type) in
revisions r76575 through r76578.
History
Date User Action Args
2022-04-11 14:56:23adminsetgithub: 44698
2009-11-28 16:40:11mark.dickinsonsetstatus: open -> closed
resolution: fixed
messages: + msg95794
2009-10-24 16:02:07mark.dickinsonsetmessages: + msg94427
2009-10-24 15:45:23mancausoftsetmessages: + msg94426
2009-10-24 15:29:30mark.dickinsonsetfiles: + issue1678380.patch

messages: + msg94424
2009-10-24 15:21:41mark.dickinsonsetstatus: closed -> open
priority: high -> normal

versions: + Python 2.6, Python 3.1, Python 2.7, Python 3.2, - Python 2.5
messages: + msg94423
type: behavior
resolution: accepted -> (no value)
stage: needs patch
2009-10-24 15:14:07mancausoftsetmessages: + msg94422
2009-10-24 14:19:33mark.dickinsonsetmessages: + msg94419
2009-10-24 14:13:15mancausoftsetnosy: + mancausoft
messages: + msg94418
2008-02-13 22:26:13mark.dickinsonsetstatus: pending -> closed
messages: + msg62370
2008-01-31 22:19:38mark.dickinsonsetstatus: open -> pending
messages: + msg61935
2008-01-21 22:58:30mark.dickinsonsetkeywords: + patch
files: + plus_minus_0j.patch
messages: + msg61462
2008-01-21 21:58:40mark.dickinsonsetmessages: + msg61455
2008-01-21 17:01:02georg.brandlsetassignee: aleax -> mark.dickinson
2007-11-09 22:24:51christian.heimessetnosy: + christian.heimes
messages: + msg57339
versions: + Python 2.5
2007-03-11 17:16:18mark.dickinsoncreate