9

The result of this comparison surprised me (CPython 3.4):

>>> 9007199254740993 == 9007199254740993.0
False

My understanding of the the docs is that the left operand should be cast to float to match the type of the right operand:

Python fully supports mixed arithmetic: when a binary arithmetic operator has operands of different numeric types, the operand with the “narrower” type is widened to that of the other, where integer is narrower than floating point, which is narrower than complex. Comparisons between numbers of mixed type use the same rule. The constructors int(), float(), and complex() can be used to produce numbers of a specific type.

This does not seem to be happening:

>>> float(9007199254740993) == 9007199254740993.0
True

What's going on here?

flornquake
  • 3,156
  • 1
  • 21
  • 32
  • 3
    Note that `9007199254740993.0` is actually `9007199254740992.0`.. – Martijn Pieters Jul 15 '15 at 17:48
  • Based on Dan's answer, you could test for equality by checking if `(9007199254740993 - 9007199254740993.0) == 0.0` (since `-` is inarguably an arithmetic operator). – larsks Jul 15 '15 at 17:49
  • As for *why*, see http://stackoverflow.com/q/1848700/3001761 – jonrsharpe Jul 15 '15 at 17:50
  • think that's weird? Try `8.9999999999999999 == 9` – krethika Jul 15 '15 at 17:52
  • I thought that's what the OP was looking for (one is already a float, and the int would be cast "to the wider type". – larsks Jul 15 '15 at 17:53
  • *"...the operand with the “narrower” type is widened to that of the other"* yes, but changing the type does not mean you can regain lost precision. – Sam Jul 15 '15 at 17:55
  • 9007199254740993 is a long, it appears the float is widened to a long in that expression. However 9007199254740993.0 cannot be exactly represented as a floating point , and then the truncation of doing long(9007199254740993.0) converts it to 9007199254740992. Though I can't find the python docs to back that up. – nos Jul 15 '15 at 17:56
  • 1
    In school I was taught to never do an exact compare with floats. Instead allow a tiny error and make use of >= and <= signs. If you want to understand why, take a look at the way floats are stored in hardware. It will quickly become apparent. – SBH Jul 15 '15 at 17:58
  • @SBHayes useful advice in general, but not relevant here – jonrsharpe Jul 15 '15 at 18:00
  • `==` is not an arithmetic operator. It's a [comparison operator](https://docs.python.org/3.4/reference/expressions.html#comparisons) – krethika Jul 15 '15 at 18:01
  • 1
    @mehtunguh note that, in this case, *it's the same rule* - both should be converted to `float`. – jonrsharpe Jul 15 '15 at 18:04
  • @jonrsharpe no, the docs I linked above say _converted to a common type_ without specifying which one. Clearly in this case they are converted to int. – krethika Jul 15 '15 at 18:08
  • Note that in the documentation you cite `==` and/or `__eq__()` as well as the rest of the comparison operators are not listed in the table of supported operations for numeric types. For this reason I would not immediately assume that the `==` operator is included in that list. Interestingly take a look at the difference in the output of the hex() and __hex__() for this value declared as an in and float var x and y: `>>> y.hex()` `'0x1.0000000000000p+53'` `>>> x.__hex__()` `'0x20000000000001'` – Matt Jul 15 '15 at 18:10

2 Answers2

9

Python doesn't exactly convert the integer to a float here; it converts the float to an integer:

>>> 9007199254740993 == int(9007199254740993.0)
False

That fails because int(9007199254740993.0) is actually 9007199254740992:

>>> 9007199254740992 == 9007199254740993.0
True

See the float_richcompare() function. Specifically, the comment before it:

/* Comparison is pretty much a nightmare. 

 [...]

 * When mixing float with an integer type, there's no good *uniform* approach.
 * Converting the double to an integer obviously doesn't work, since we
 * may lose info from fractional bits.  Converting the integer to a double
 * also has two failure modes:  (1) an int may trigger overflow (too
 * large to fit in the dynamic range of a C double); (2) even a C long may have
 * more bits than fit in a C double (e.g., on a 64-bit box long may have
 * 63 bits of precision, but a C double probably has only 53), and then
 * we can falsely claim equality when low-order integer bits are lost by
 * coercion to double.  So this part is painful too.

What happens for these two numbers is this:

  • Python tries the int.__eq__(float) route, but that returns NotImplemented
  • Python tries the float.__eq__(int) route, which float_richcompare() handles.

In that function v is your float, w is the integer. The following is a selection of code that is executed for that path:

else if (PyLong_Check(w)) {   /* true because the other number is an Python integer */

    /* ... */

    nbits = _PyLong_NumBits(w);   /* 54 for your integer */

    /* ... */

    if (nbits <= 48) {  /* nope, can't make it a float outright */
        /* ... */
    }

    (void) frexp(i, &exponent);  /* the exponent is 54 for your float */

    if (exponent < 0 || (size_t)exponent < nbits) {
        /* not true */
    }
    if ((size_t)exponent > nbits) {
        /* also not true */
    }
    /* v and w have the same number of bits before the radix
     * point.  Construct two ints that have the same comparison
     * outcome.
     */
    {
        /* code to convert v to an integer vv, copy w to ww */

        r = PyObject_RichCompareBool(vv, ww, op);

        /* ... */

        result = PyBool_FromLong(r);

        /* ... */

        return result;
    }

So in the end due to the size of the numbers involved, Python converts the float to an integer, and it is that conversion where the floating point number turns out to be 9007199254740992. That's because a float cannot actually express 9007199254740993.0 accurately:

>>> 9007199254740993.0
9007199254740992.0
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
0

I think Martijn Pieters' answer while ultimately correct massively overcomplicating things. 9007199254740993.0 is not a valid float value in Python (because it is not a valid IEEE 754 64-bit floating point value).

Python will silently round 9007199254740993.0 to the nearest representable value during parsing / conversion, which is 9007199254740992.0:

>>> 9007199254740993.0
9007199254740992.0
>>> float(9007199254740993)
9007199254740992.0

Knowing that it's easy to see why the following happens:

>>> 9007199254740993.0 == 9007199254740993
False

>>> 9007199254740993.0 == 9007199254740992
True
orlp
  • 112,504
  • 36
  • 218
  • 315