Floating Point Improvements
The rejected PEP-754 attempted to make Python fully IEEE 754 compliant. It was rejected because Python (the CPython implementation) relies on the underlying C library to handle IEEE 754 special values such as
NaN (not a number), and positive and negative infinity. There are many inconsistencies across different platforms. Still, Python 3.0 (and 2.6 too), incorporated many floating point improvements, and both implement the IEEE 754 standard much more closely.
The
float() function that turns strings into floating point numbers now understands
nan,
+inf (or
inf), and
-inf and turns them into the Not A Number, Positive and Negative Infinity IEEE 754 values. (Case doesn't matter, so
NaN,
INF, etc., are valid too.)
The math module now has the functions
isnan() and
isinf(). The
isinf() function doesn't distinguish between
inf,
+inf and
-inf. Here are some examples:
>>> float('nan')
nan
>>> float('NaN') # Any case works
nan
>>> float('+inf')
inf
>>> float('-inf')
-inf
>>> float('INF')
inf
>>> float('nan') + float('inf')
nan
>>> float('inf') + float('-inf')
nan
>>> float('inf') - float('-inf')
inf
>>> import math
>>> math.isnan(float('nan'))
True
>>> math.isinf(float('inf'))
True
>>> math.isinf(float('-inf'))
True
>>> math.isinf(float('nan'))
False
>>> math.isnan(float('-inf'))
False
The math module has now also a
copysign(x, y) function that returns the absolute value of
x with the sign of
y. I don't understand why this function exists instead of a simple
sign() function that returns
-1,
1 or a couple of
ispositive(),
isnegative() functions. The documentation is very succinct:
>>> help(math.copysign)
Help on built-in function copysign in module math:
copysign(...)
copysign(x,y)
Return x with the sign of y.
However,
copysign works as advertised except for
NaN. If you try to copy the sign of
NaN you get inconsistent results—a negative sign on Mac OS X and a positive sign on Windows. A
closed bug says this behavior is OK. I disagree.
NaN is not a number, and as such has no sign. Trying to copy the sign of
NaN is like trying to copy the sign of any other non-number value (string, list, object) and should result in an exception.
Some other functions related to floating point numbers were added to the math module too.
math.fsum() adds up the stream of numbers from an iterable, and is careful to avoid loss of precision by using partial sums (unlike the built-in
sum() function). If any of the numbers are
NaN, the result is
NaN. If the partial sum reaches
+inf or
-inf, the
sum() function returns that as the result. The
math.fsum() function raises an OverflowError exception, which is more in the spirit of IEEE 754:
>>> sum([1e308, 1, -1e308])
0.0
>>> math.fsum([1e308, 1, -1e308])
1.0
>>> sum([1e100, 1, -1e100, -1])
-1.0
>>> math.fsum([1e100, 1, -1e100, -1])
0.0
>>> x = [1e308, 1e308, -1e308]
>>> sum(x)
inf
>>> math.fsum(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OverflowError: intermediate overflow in fsum
>>> sum([float('nan'), 3.3])
nan
>>> math.fsum([float('nan'), -float('nan')])
nan
The functions
acosh(),
asinh(), and
atanh() compute inverse hyperbolic functions. The
log1p() function returns the natural logarithm of
1+x (base e). The
trunc() function rounds a number toward zero, returning the closest integer value:
>>> math.acosh(30)
4.0940666686320855
>>> math.acosh(1)
0.0
>>> math.asinh(1)
0.88137358701954305
>>> math.asinh(0)
0.0
>>> math.atanh(0.5)
0.54930614433405489
>>> math.log1p(2)
1.0986122886681098
>>> math.trunc(-1.1)
-1
>>> math.trunc(-1.9)
-1
>>> math.trunc(1.1)
1
>>> math.trunc(1.9)
1
>>> math.trunc(3.0)
3
You can convert floating-point numbers to or from hexadecimal strings. The conversion functions convert floats to and from a string representation without introducing rounding errors from the conversion between decimal and binary (if there are enough digits to represent the number fully). Floats have a
hex() method that returns a string representation, while the
float.fromhex() method converts a string back into a number (as accurately as possible):
>>> x = 4.2
>>> a.hex()
'0x1.0cccccccccccdp+2'
>>> float.fromhex('0x1.0cccccccccccdp+2')
4.2000000000000002
The decimal module was updated to version 1.66 of the General Decimal Specification. New features include some methods for some basic mathematical functions such as
exp() and
log10():
>>> Decimal(1).exp()
Decimal("2.718281828459045235360287471")
>>> Decimal("2.7182818").ln()
Decimal("0.9999999895305022877376682436")
>>> Decimal(1000).log10()
Decimal("3")
The
as_tuple() method of Decimal objects now returns a named tuple (more on named tuples in future article) with sign, digits, and exponent fields:
>>> Decimal('-3.3').as_tuple()
DecimalTuple(sign=1, digits=(3, 3), exponent=-1)
A new variable in the sys module,
float_info, is an object that contains information derived from the
float.h file about the platform's floating-point support:
>>> sys.float_info
sys.floatinfo(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308,
min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, dig=15,
mant_dig=53, epsilon=2.2204460492503131e-16, radix=2, rounds=1)
Overall, Python has definitely elevated its level of support for floating point numbers—but it is not perfect yet. The
Numpy external package is still the best tool for serious number crunching in Python.