9

The documentation about numeric types states that:

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.

This is supported by the following behavior:

>>> int.__eq__(1, 1.0)
NotImplemented
>>> float.__eq__(1.0, 1)
True

However for large integer numbers something else seems to happen since they won't compare equal unless explicitly converted to float:

>>> n = 3**64
>>> float(n) == n
False
>>> float(n) == float(n)
True

On the other hand, for powers of 2, this doesn't seem to be a problem:

>>> n = 2**512
>>> float(n) == n
True

Since the documentation implies that int is "widened" (I assume converted / cast?) to float I'd expect float(n) == n and float(n) == float(n) to be similar but the above example with n = 3**64 suggests differently. So what rules does Python use to compare int to float (or mixed numeric types in general)?


Tested with CPython 3.7.3 from Anaconda and PyPy 7.3.0 (Python 3.6.9).

22
  • 4
    You're probably just hitting float precision limits and float(3**64) is not exactly equal to int(3**64). Commented Jan 31, 2020 at 14:35
  • 2
    float(2**512), on the other hand, can be represented precisely, despite being much larger, because it is a power of 2. The mantissa needs only 1 bit for full precision, and the exponent only needs 9. Commented Jan 31, 2020 at 14:38
  • 3
    I understand all of this behavior but the the documentation states that the "narrower" type, in this case int, would be widened to the other, in this case float. So there shouldn't be any issue with precision loss due to round tripping; I'd expect float(n) == n to be internally handled such that the r.h.s. is converted to float, i.e. to be similar to float(n) == float(n). Commented Jan 31, 2020 at 14:42
  • 3
    If I recall correctly, Python, or at least some implementations of it, goes to pains to compare values exactly. When 3**64 is compared to float(n), it is not converted to float. Rather, the exact mathematical value of 3**64 is compared to the exact mathematical value of float(n). Since they differ, the result is False. When you convert 3**64 to float, it converts to a value representable in the float type, introducing some error. The wording in the documentation is unfortunate in saying that float is “wider” than int. Generally, it cannot represent all int values… Commented Jan 31, 2020 at 14:44
  • 4
    github.com/python/cpython/blob/master/Objects/… appears to be responsible for float-and-int comparison. It's certainly doing more than just converting the int to a float and comparing, but my C-reading abilities are not strong enough to know what exactly it is doing. Commented Jan 31, 2020 at 14:44

1 Answer 1

5

The language specification on value comparisons contains the following paragraph:

Numbers of built-in numeric types (Numeric Types — int, float, complex) and of the standard library types fractions.Fraction and decimal.Decimal can be compared within and across their types, with the restriction that complex numbers do not support order comparison. Within the limits of the types involved, they compare mathematically (algorithmically) correct without loss of precision.

This means when two numeric types are compared, the actual (mathematical) numbers that are represented by these objects are compared. For example the numeral 16677181699666569.0 (which is 3**34) represents the number 16677181699666569 and even though in "float-space" there is no difference between this number and 16677181699666568.0 (3**34 - 1) they do represent different numbers. Due to limited floating point precision, on a 64-bit architecture, the value float(3**34) will be stored as 16677181699666568 and hence it represents a different number than the integer numeral 16677181699666569. For that reason we have float(3**34) != 3**34 which performs a comparison without loss of precision.

This property is important in order to guarantee transitivity of the equivalence relation of numeric types. If int to float comparison would give similar results as if the int object would be converted to a float object then the transitive relation would be invalidated:

>>> class Float(float):
...     def __eq__(self, other):
...         return super().__eq__(float(other))
... 
>>> a = 3**34 - 1
>>> b = Float(3**34)
>>> c = 3**34
>>> a == b
True
>>> b == c
True
>>> a == c  # transitivity demands that this holds true
False

The float.__eq__ implementation on the other hand, which considers the represented mathematical numbers, doesn't infringe that requirement:

>>> a = 3**34 - 1
>>> b = float(3**34)
>>> c = 3**34
>>> a == b
True
>>> b == c
False
>>> a == c
False

As a result of missing transitivity the order of the following list won't be changed by sorting (since all consecutive numbers appear to be equal):

>>> class Float(float):
...     def __lt__(self, other):
...         return super().__lt__(float(other))
...     def __eq__(self, other):
...         return super().__eq__(float(other))
... 
>>> numbers = [3**34, Float(3**34), 3**34 - 1]
>>> sorted(numbers) == numbers
True

Using float on the other hand, the order is reversed:

>>> numbers = [3**34, float(3**34), 3**34 - 1]
>>> sorted(numbers) == numbers[::-1]
True
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.