I wonder why a - b
and a + (-b)
give the same result but in different types in numpy
:
import numpy as np
minuend = np.array(1, dtype=np.int64)
subtrahend = 1 << 63
result_minus = minuend - subtrahend
result_plus = minuend + (-subtrahend)
print(result_minus == result_plus) # True
print(type(result_minus)) # float64
print(type(result_plus)) # int64
Why is that, and where can I read about it?
The point is that 1 << 63
cannot be represented using a int64
type, but -(1 << 63)
can. This is a pathological case coming from how signed integers are represented in binary (C2 representation).
On one hand, Numpy converts subtrahend
(1 << 63
) to a uint64
value because int64
is too small to hold the value.
On another hand, -subtrahend
is computed by CPython so it results in a pure-Python integer containing -(1 << 63)
. The value is then converted by Numpy to a int64
value (because this type is large enough to hold the value).
Numpy only operates on arrays of the same types internally. Binary operations involving different types result in array promotions (inherited from the C language mainly because Numpy is written in C) : Numpy converts the type of the input arrays so the target binary operation makes sense and is also safe (no overflow, at the expense of a possible loss of precision in pathological cases).
In this case, when the subtraction is performed, Numpy chooses to store the final array in a float64
array because a binary operation on both uint64
and int64
array is likely to cause overflows (unsigned integers are too big to be stored in signed ones and negative signed integer cannot be represented in unsigned ones). When the addition is performed, the two array/values are of the same type (i.e. int64
), so there is no need for any promotion and the resulting array is of type int64
.
Here is a way to see that:
>>> np.int64(1 << 63)
---------------------------------------------------------------------------
OverflowError Traceback (most recent call last)
Cell In [7], line 1
----> 1 np.int64(1 << 63)
OverflowError: Python int too large to convert to C long
>>> np.int64(-(1 << 63))
-9223372036854775808
>>> np.array(subtrahend).dtype
dtype('uint64')
# Numpy first convert both to float64 arrays to avoid overflows in this specific case due to mixed uint64+int64 integer types
>>> (np.array(subtrahend) + minuend).dtype
dtype('float64')
>>> np.array(-subtrahend).dtype
dtype('int64')