[Python-ideas] PEP 485: A Function for testing approximate equality

Andrew Barnert abarnert at yahoo.com
Thu Feb 5 23:46:21 CET 2015


First, +0.5 overall. The only real downside is that it takes math further away from C math.h, which is not much of a downside.

And now, the nits:

On Thursday, February 5, 2015 9:14 AM, Chris Barker <chris.barker at noaa.gov> wrote:

>Non-float types
>---------------
>
>The primary use-case is expected to be floating point numbers.
>However, users may want to compare other numeric types similarly. In
>theory, it should work for any type that supports ``abs()``,
>comparisons, and subtraction.  The code will be written and tested to

>accommodate these types:

Surely the type also has to support multiplication (or division, if you choose to implement it that way) as well, right? 

Also, are you sure your implementation doesn't need any explicit isinf/isnan checks? You mentioned something about cmath.isnan for complex numbers.

Also, what types like datetime, which support subtraction, but the result of subtraction is a different type, and only the latter supports abs and multiplication? An isclose method can be written that handles those types properly, but one can also be written that doesn't. I don't think it's important to support it (IIRC, numpy.isclose doesn't work on datetimes...), but it might be worth mentioning that, as a consequence of the exact rule being used, datetimes won't work (possibly even saying "just as with np.isclose", if I'm remembering right).
>unittest assertion
>-------------------


Presumably this is just copying assertAlmostEqual and replacing the element comparisons with a call to isclose, passing the params along?


>How much difference does it make?

>---------------------------------

This section relies on transformations that are valid for reals but not for floats. In particular:

>  delta = tol * (a-b)
>
>or::
>
>  delta / tol = (a-b)


If a and b are subnormals, the multiplication could underflow to 0, but the division won't; if delta is a very large number, the division could overflow to inf but the multiplication won't. (I'm assuming delta < 1.0, of course.) Your later substitution has the same issue. I think your ultimate conclusion is right, but your proof doesn't really prove it unless you take the extreme cases into account.
>The case that uses the arithmetic mean of the two values requires that
>the value be either added together before dividing by 2, which could
>result in extra overflow to inf for very large numbers, or require
>each value to be divided by two before being added together, which

>could result in underflow to -inf for very small numbers.

Dividing by two results in underflow to 0, not -inf. (Adding before dividing can result in overflow to -inf just as it can to inf, of course, but I don't think that needs mentioning.) 

Also, dividing by two only results in underflow to 0 for two numbers, +/-5e-324, and I don't think there's any case where that underflow can cause a problem where you wouldn't already underflow to 0 unless tol >= 1.0, so I'm not sure this is actually a problem to worry about. That being said, your final conclusion is again hard to argue with: the benefit is so small it's not really worth _any_ significant cost, even the cost of having to explain why there's no cost. :)

>Relative Tolerance Default
>--------------------------


This section depends on the fact that a Python float has about 1e-16 precision. But technically, that isn't true; it has about sys.float_info.epsilon precision, which is up to the implementation; CPython leaves it up to the C implementation's double type, and C89 leaves that up to the platform. That's why sys.float_info.epsilon is available in the first place--because you can't know it a priori.


In practice, I don't know of any modern platforms that don't use IEEE double or something close enough, so it's always going to be 2.2e-16-ish. And I don't think you want the default tolerance to be platform-dependent. And I don't think you want to put qualifiers in all the half-dozen places you mention the 1e-9. I just think it's worth mentioning in a parenthetical somewhere in this paragraph, like maybe:

>The relative tolerance required for two values to be considered
>"close" is entirely use-case dependent. Nevertheless, the relative
>tolerance needs to be less than 1.0, and greater than 1e-16

>(approximate precision of a python float *on almost all platforms*).
>Absolute tolerance default
>--------------------------
>
>The absolute tolerance value will be used primarily for comparing to
>zero. The absolute tolerance required to determine if a value is
>"close" to zero is entirely use-case dependent. There is also
>essentially no bounds to the useful range -- expected values would
>conceivably be anywhere within the limits of a python float.  Thus a

>default of 0.0 is selected.

Does that default work properly for the other supported types? Or do you need to use the default-constructed or zero-constructed value for some appropriate type? (Sorry for C++ terminology, but you know what I mean--and the C++ rules do work for these Python types.) Maybe it's better to just specify "zero" and leave it up to the implementation to do the trivial thing if possible or something more complicated if necessary, rather than specifying that it's definitely the float 0.0?

>Expected Uses
>=============
>
>The primary expected use case is various forms of testing -- "are the
>results computed near what I expect as a result?" This sort of test
>may or may not be part of a formal unit testing suite. Such testing
>could be used one-off at the command line, in an iPython notebook,
>part of doctests, or simple assets in an ``if __name__ == "__main__"``
>block.


Typo: "...or simple *asserts* in an..."

>The proposed unitest.TestCase assertion would have course be used in

>unit testing.

Typo: "...of course...".


More information about the Python-ideas mailing list