pickle broken: can't handle NaN or Infinity under win32

G

Grant Edwards

I finally figured out why one of my apps sometimes fails under
Win32 when it always works fine under Linux: Under Win32, the
pickle module only works with a subset of floating point
values. In particular the if you try to dump/load an infinity
or nan value, the load operation chokes:


Under Linux:

$ python
Python 2.3.4 (#2, Feb 9 2005, 14:22:48)
[GCC 3.4.1 (Mandrakelinux 10.1 3.4.1-4mdk)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
$ python pickletest.py
(inf, nan) (inf, nan)


Under Win32:

$ python
ActivePython 2.3.4 Build 233 (ActiveState Corp.) based on
Python 2.3.4 (#53, Oct 18 2004, 20:35:07) [MSC v.1200 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
$ python pickletest.py
Traceback (most recent call last):
File "pickletest.py", line 8, in ?
d = pickle.loads(s)
File "C:\PYTHON23\lib\pickle.py", line 1394, in loads
return Unpickler(file).load()
File "C:\PYTHON23\lib\pickle.py", line 872, in load
dispatch[key](self)
File "C:\PYTHON23\lib\pickle.py", line 968, in load_float
self.append(float(self.readline()[:-1]))
ValueError: invalid literal for float(): 1.#INF

I realize that this is probably due to underlying brokenness in
the Win32 libc implimentation, but should the pickle module
hide such platform-dependancies from the user?

Best case, it would be nice if pickle could handle all floats
in a portable way.

Worst case, shouldn't the pickle module documentation mention
that pickling floats non-portable or only partially implimented?

On a more immediate note, are there hooks in pickle to allow
the user to handle types that pickle can't deal with? Or, do I
have to throw out pickle and write something from scratch?

[NaN and Infinity are prefectly valid (and extremely useful)
floating point values, and not using them would require huge
complexity increases in my apps (not using them would probably
at least triple the amount of code required in some cases).]
 
G

Grant Edwards

I finally figured out why one of my apps sometimes fails under
Win32 when it always works fine under Linux
[...]

Oh, I forgot, here's pickletest.py:

#!/usr/bin/python
import pickle

f1 = (1e300*1e300)
f2 = f1/f1
o = (f1,f2)
s = pickle.dumps(o)
d = pickle.loads(s)

print o,d
$ python pickletest.py
(inf, nan) (inf, nan)

Under Win32:

$ python pickletest.py
Traceback (most recent call last):
File "pickletest.py", line 8, in ?
d = pickle.loads(s)
File "C:\PYTHON23\lib\pickle.py", line 1394, in loads
return Unpickler(file).load()
File "C:\PYTHON23\lib\pickle.py", line 872, in load
dispatch[key](self)
File "C:\PYTHON23\lib\pickle.py", line 968, in load_float
self.append(float(self.readline()[:-1]))
ValueError: invalid literal for float(): 1.#INF
 
S

Scott David Daniels

Grant said:
I finally figured out why one of my apps sometimes fails under
Win32 when it always works fine under Linux: Under Win32, the
pickle module only works with a subset of floating point
values. In particular the if you try to dump/load an infinity
or nan value, the load operation chokes:
There is no completely portable way to do this. Any single platform
can have a solution, but (since the C standards don't address how
NaNs and Infs are represented) there is not a good portable way to do
the pickle / unpickle. It is nice the exception is raised, since at
one point it was not (and a simple 1.0 was returned).
See explanations in article 654866:

http://sourceforge.net/tracker/index.php?func=detail&aid=714733&group_id=5470&atid=105470
$ python pickletest.py
Traceback (most recent call last): ...
File "C:\PYTHON23\lib\pickle.py", line 968, in load_float
self.append(float(self.readline()[:-1]))
ValueError: invalid literal for float(): 1.#INF
I realize that this is probably due to underlying brokenness in
the Win32 libc implimentation, but should the pickle module
hide such platform-dependancies from the user?
As mentioned above, there is no C standard-accessible way to
predictably build or represent NaNs, negative zeroes, or Infinities.
[NaN and Infinity are prefectly valid (and extremely useful)
floating point values, and not using them would require huge
complexity increases in my apps (not using them would probably
at least triple the amount of code required in some cases).]

You could check to see if the Python 2.5 pickling does a better
job. Otherwise, you've got your work cut out for you.

-Scott David Daniels
(e-mail address removed)
 
G

Grant Edwards

There is no completely portable way to do this.

Python deals with all sorts of problems for which there is no
completely portable solution. Remember: "practicality beats purity."
Any single platform can have a solution, but (since the C
standards don't address how NaNs and Infs are represented)
there is not a good portable way to do the pickle / unpickle.

Likewise, there is no completely portable python
implimentation. Any single platform can have a Python
implimentation, but since the C standards don't address a
universal standard for "a computer" there is not a good
portable way to do Python. I guess we'd better give up on
Python. :)
It is nice the exception is raised, since at one point it was
not (and a simple 1.0 was returned).

That would be even worse.
[NaN and Infinity are prefectly valid (and extremely useful)
floating point values, and not using them would require huge
complexity increases in my apps (not using them would probably
at least triple the amount of code required in some cases).]

You could check to see if the Python 2.5 pickling does a better
job. Otherwise, you've got your work cut out for you.

Fixing it is really quite trivial. It takes less than a dozen
lines of code. Just catch the exception and handle it.

def load_float(self):
s = self.readline()[:-1]
try:
f = float(s)
except ValueError:
s = s.upper()
if s in ["1.#INF", "INF"]:
f = 1e300*1e300
elif s in ["-1.#INF", "-INF"]:
f = -1e300*1e300
elif s in ["NAN","1.#QNAN","QNAN","1.#IND","IND","-1.#IND"]:
f = -((1e300*1e300)/(1e300*1e300))
else:
raise ValueError, "Don't know what to do with "+`s`
self.append(f)

Obviously the list of accepted string values should be expanded
to include other platforms as needed. The above example
handles Win32 and glibc (e.g. Linux).

Even better, add that code to float().
 
S

Scott David Daniels

Grant said:
Python deals with all sorts of problems for which there is no
completely portable solution. Remember: "practicality beats purity."

...
Fixing it is really quite trivial. It takes less than a dozen
lines of code. Just catch the exception and handle it.
Since you know it is quite trivial, and I don't, why not submit a
patch resolving this issue. Be sure to include tests for all
supported Python platforms.

--Scott David Daniels
(e-mail address removed)
 
G

Grant Edwards

Since you know it is quite trivial, and I don't, why not
submit a patch resolving this issue. Be sure to include tests
for all supported Python platforms.

I'm working on it. I should have said it's trivial if you have
access to the platforms to be supported. I've tested a fix
that supports pickle streams generated under Win32 and glibc.
That's using the "native" string representation of a NaN or
Inf.

A perhaps simpler approach would be to define a string
representation for Python to use for NaN and Inf. Just because
something isn't defined by the C standard doesn't mean it can't
be defined by Python.
 
S

Scott David Daniels

Grant said:
I'm working on it. I should have said it's trivial if you have
access to the platforms to be supported. I've tested a fix
that supports pickle streams generated under Win32 and glibc.
That's using the "native" string representation of a NaN or
Inf.
Several issues:
(1) The number of distinct NaNs varies among platforms. There are
quiet and signaling NaNs, negative 0, the NaN that Windows VC++
calls "Indeterminate," and so on.
(2) There is no standard-conforming way to create these values.
(3) There is no standard-conforming way to detect these values.

--Scott David Daniels
(e-mail address removed)
 
T

Terry Reedy

Grant Edwards said:
I'm working on it. I should have said it's trivial if you have
access to the platforms to be supported. I've tested a fix
that supports pickle streams generated under Win32 and glibc.
That's using the "native" string representation of a NaN or
Inf.

A perhaps simpler approach would be to define a string
representation for Python to use for NaN and Inf. Just because
something isn't defined by the C standard doesn't mean it can't
be defined by Python.

I believe that changes have been made to marshal/unmarshal in 2.5 CVS with
respect to NAN/INF to eliminate annoying/surprising behavior differences
between corresponding .py and .pyc files. Perhaps these revisions would be
relevant to pickle changes.

TJR
 
G

Grant Edwards

Several issues:
(1) The number of distinct NaNs varies among platforms.

According to the IEEE standard, there are exactly two:
signalling and quiet, and on platforms that don't impliment
floating point exceptions (probably in excess of 99.9% of
python installations), the difference between the two is moot.
There are quiet and signaling NaNs, negative 0,

Negative 0 isn't a NaN, it's just negative 0.
the NaN that Windows VC++ calls "Indeterminate," and so
on.

That's just Microsoft's way of spelling "signalling NaN."
(2) There is no standard-conforming way to create these values.

What standard are you looking at? My copy of the IEEE 754
standard is pretty clear.
(3) There is no standard-conforming way to detect these
values.

The bit patterns are defined by the IEEE 754 standard. If
there are Python-hosting platoforms that don't use IEEE 754 as
the floating point representation, then that can be dealt with.

Python has _tons_ of platform-specific code in it.

Why all of a sudden is it taboo for Python to impliment
something that's not universally portable and defined in a
standard? Where's the standard defining Python?
 
T

Terry Reedy

Grant Edwards said:
The bit patterns are defined by the IEEE 754 standard. If
there are Python-hosting platoforms that don't use IEEE 754 as
the floating point representation, then that can be dealt with.

Python has _tons_ of platform-specific code in it.

More, I believe, than the current maintainers would like. Adding more
would probably require a commitment to maintain the addition (respond to
bug reports) for a few years.
Why all of a sudden is it taboo for Python to impliment
something that's not universally portable and defined in a
standard?

??

Perhaps you wrote this before reading my last post reporting that some
NaN/Inf changes have already been made for 2.5. I believe that more would
be considered if properly submitted.
Where's the standard defining Python?

The Language and Library Reference Manuals at python.org.

Terry J. Reedy
 
S

Scott David Daniels

Grant said:
According to the IEEE standard, there are exactly two:
signalling and quiet, and on platforms that don't impliment
floating point exceptions (probably in excess of 99.9% of
python installations), the difference between the two is moot.
But it does not specify the representation of such NaNs.
Negative 0 isn't a NaN, it's just negative 0.
Right, but it is hard to construct in standard C.
What standard are you looking at? My copy of the IEEE 754
standard is pretty clear.
I was talking about the C89 standard. In the absence of a C
standard way to create, manipulate, and test for these values,
you must implement and test platform-by-platform.
The bit patterns are defined by the IEEE 754 standard.
Perhaps this is right and I misunderstand the standard, but my
understanding is that the full bit pattern is not, in fact,
defined.
If there are Python-hosting platoforms that don't use IEEE 754 as
the floating point representation, then that can be dealt with.
There are.
Python has _tons_ of platform-specific code in it.
But the _tons_ are written in C89 C.
Why all of a sudden is it taboo for Python to impliment
something that's not universally portable and defined in a
standard? Where's the standard defining Python?
It is not taboo. I am trying to explain why it is not a
trivial task, but a substantial effort. If you are willing
to perform the substantial effort, good on you, and I'll help.
If you simply want to implement on the two platforms you use,
and want everyone else to implement the interface you choose,
that seems to me an unreasonable request.

--Scott David Daniels
(e-mail address removed)
 
S

Scott David Daniels

Paul said:
Huh? It's just a hex constant.
Well, -0.0 doesn't work, and (double)0x80000000 doesn't work,
and.... I think you have to use quirks of a compiler to create
it. And I don't know how to test for it either, x < 0.0 is
not necessarily true for negative 0.

I am not trying to say there is no way to do this. I am
trying to say it takes thought and effort on every detail,
in the definition, implementations, and unit tests.

--Scott David Daniels
(e-mail address removed)
 
P

Paul Rubin

Scott David Daniels said:
Well, -0.0 doesn't work, and (double)0x80000000 doesn't work,
and.... I think you have to use quirks of a compiler to create
it. And I don't know how to test for it either, x < 0.0 is
not necessarily true for negative 0.

Aren't we talking about IEEE 754 arithmetic? There's some specific
bit pattern(s) for -0.0 and you can assign a float variable to
such a pattern.
 
T

Tim Peters

[with the start of US summer comes the start of 754 ranting season]

[Grant Edwards]
[Scott David Daniels]
[Paul Rubin]
[Scott David Daniels]
Well, -0.0 doesn't work,

C89 doesn't define the result of that, but "most" C compilers these
days will create a negative 0.
and (double)0x80000000 doesn't work,

In part because that's an integer <wink>, and in part because it's
only 32 bits. It requires representation casting tricks (not
conversion casting tricks like the above), knowledge of the platform
endianness, and knowledge of the platform integer sizes. Assuming the
platform uses 754 bit layout to begin with, of course.
and.... I think you have to use quirks of a compiler to create
it.

You at least need platform knowledge. It's really not hard, if you
can assume enough about the platform.
And I don't know how to test for it either, x < 0.0 is
not necessarily true for negative 0.

If it's a 754-conforming C compiler, that's necessarily false (+0 and
-0 compare equal in 754). Picking the bits apart is again the closest
thing to a portable test. Across platforms with a 754-conforming
libm, the most portable way is via using atan2(!):
-3.1415926535897931

It's tempting to divide into 1, then check the sign of the infinity,
but Python stops you from doing that:
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ZeroDivisionError: float division

That can't be done at the C level either, because _some_ people run
Python with their 754 HW floating-point zero-division, overflow, and
invalid operation traps enabled, and then anything like division by 0
causes the interpreter to die. The CPython implementation is
constrained that way.

Note that Python already has Py_IS_NAN and Py_IS_INFINITY macros in
pyport.h, and the Windows build maps them to appropriate
Microsoft-specific library functions. I think it's stuck waiting on
others to care enough to supply them for other platforms. If a
platform build doesn't #define them, a reasonable but cheap attempt is
made to supply "portable" code sequences for them, but, as the
pyport.h comments note, they're guaranteed to do wrong things in some
cases, and may not work at all on some platforms. For example, the
default

#define Py_IS_NAN(X) ((X) != (X))

is guaranteed never to return true under MSVC 6.0.
I am not trying to say there is no way to do this. I am
trying to say it takes thought and effort on every detail,
in the definition, implementations, and unit tests.

It's par for the course -- everyone thinks "this must be easy" at
first, and everyone who persists eventually gives up. Kudos to
Michael Hudson for persisting long enough to make major improvements
here in pickle, struct and marshal for Python 2.5!
 
G

Grant Edwards

But it does not specify the representation of such NaNs.

Yes, it does. It specifies it exactly: certain bits are ones,
certain other bits are zeros. I don't know how much more exactly
the representation can be defined.
Perhaps this is right and I misunderstand the standard, but my
understanding is that the full bit pattern is not, in fact,
defined.

The represntation of NaNs, infinities, normalized numbers and
denormal numbers are all completely defined by the standard.
There are.

That's where it gets nasty.
But the _tons_ are written in C89 C.
True.

It is not taboo. I am trying to explain why it is not a
trivial task, but a substantial effort.

It's trivial for platforms that obey the IEEE 754 standard.
If you are willing to perform the substantial effort, good on
you, and I'll help. If you simply want to implement on the two
platforms you use, and want everyone else to implement the
interface you choose, that seems to me an unreasonable
request.

I would think that implimenenting things according to the IEEE
standard and letting non-standard platforms figure out what to
do for themselves would seem a reasonable approach.
 
G

Grant Edwards

Huh? It's just a hex constant.

Yup. There are two ways to construct a NaN. One is to do
something like (1e300*1e300)/(1e300*1e300) and hope for the
best. The other is to assume IEEE 754 just use 7f800000 or
7fc00000 depending on whether you want a signalling or quiet
NaN.
 
G

Grant Edwards

C89 doesn't define the result of that, but "most" C compilers these
days will create a negative 0.

I think you meant something like

float f;
*((uint32_t*)&d) = 0xNNNNNNNN;
If it's a 754-conforming C compiler, that's necessarily false (+0 and
-0 compare equal in 754). Picking the bits apart is again the closest
thing to a portable test. Across platforms with a 754-conforming
libm, the most portable way is via using atan2(!):

[brain-bending example elided]

It's probobly because of the domains in which I work, but I
don't think I've ever cared whether a zero is positive or
negative. I understand why it's easier to impliment things
that way, but I don't see why anybody would care. OTOH, NaNs
and Infinities are indisposable for real-world stuff.
It's par for the course -- everyone thinks "this must be easy"
at first, and everyone who persists eventually gives up.
Kudos to Michael Hudson for persisting long enough to make
major improvements here in pickle, struct and marshal for
Python 2.5!

I would think it doable if one assumed IEEE-754 FP (famous last
words). I suppose there are still a few VAX machines around.
And there are things like TI DSPs that don't use IEEE-754.
 
G

Grant Edwards

I think you meant something like

float f;
*((uint32_t*)&d) = 0xNNNNNNNN;

*((uint32_t*)&f) = 0xNNNNNNNN;

It doesn't matter how many times one proofreads things like
that...
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,057
Latest member
KetoBeezACVGummies

Latest Threads

Top