numpy 意外的uint 64行为0xFFFF'FFFF' FFFF'FFFF - 1 = 0?

xzlaal3s  于 2023-05-07  发布在  其他
关注(0)|答案(5)|浏览(111)

考虑以下简短的numpy会话,展示uint64数据类型

import numpy as np
 
a = np.zeros(1,np.uint64)
 
a
# array([0], dtype=uint64)
 
a[0] -= 1
a
# array([18446744073709551615], dtype=uint64)
# this is 0xffff ffff ffff ffff, as expected

a[0] -= 1
a
# array([0], dtype=uint64)
# what the heck?

我完全被最后的输出搞糊涂了。
0xFFFF FFFF FFFF FFFF
这到底是怎么回事
我的设置:

>>> sys.platform
'linux'
>>> sys.version
'3.10.5 (main, Jul 20 2022, 08:58:47) [GCC 7.5.0]'
>>> np.version.version
'1.23.1'
mgdq6dx1

mgdq6dx11#

默认情况下,NumPy将Python int对象转换为numpy.int_,这是一个对应于C long的有符号整数dtype。(这个决定是在早期做出的,当时Python int * 也 * 对应于C long
没有足够大的整数dtype来容纳numpy.uint64 dtype * 和 * numpy.int_ dtype的所有值,因此numpy.uint64标量和Python int对象之间的操作产生float 64结果而不是整数结果。(uint 64 arrays 和Python int之间的操作可能会有不同的行为,因为int会根据其值转换为dtype,但a[0]是标量。
第一次减法运算产生一个值为-1的float 64,第二次减法运算产生一个值为264的float 64(因为float 64没有足够的精度来精确地执行减法运算)。这两个值都超出了uint 64 dtype的范围,因此转换回uint 64以赋值给a[0]会产生undefined behavior(从C继承- NumPy只使用C强制转换)。
在您的计算机上,这会产生回绕行为,因此-1回绕到18446744073709551615,2
64回绕到0,但这并不能保证。您可能会在其他设置中看到不同的行为。评论中的人确实看到了不同的行为。

mm5n2pyu

mm5n2pyu2#

a[0] - 11.8446744073709552e+19,a numpy.float64。它不能保留所有的精度,所以它的值是18446744073709551616=264。当用dtype np.uint64写回a时,它变成0

wpcxdonn

wpcxdonn3#

所有现有的答案都是正确的。我只是想在Windows 10上添加我得到了不同的结果,即9223372036854775808。
复制步骤:

Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.13.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import numpy as np

In [2]: a = np.zeros(1,np.uint64)

In [3]: a
Out[3]: array([0], dtype=uint64)

In [4]: a[0] -= 1

In [5]: a
Out[5]: array([18446744073709551615], dtype=uint64)

In [6]: a[0] - 1
Out[6]: 1.8446744073709552e+19

In [7]: a[0] - 1 == 2**64
Out[7]: True

In [8]: a[0] -= 1
<ipython-input-8-9ab639258820>:1: RuntimeWarning: invalid value encountered in cast
  a[0] -= 1

In [9]: a
Out[9]: array([9223372036854775808], dtype=uint64)

In [10]: f'{a[0]:b}'
Out[10]: '1000000000000000000000000000000000000000000000000000000000000000'

In [11]: len(_)
Out[11]: 64

In [12]: a[0] == 2**63
Out[12]: True

In [13]: a[0] - 1
Out[13]: 9.223372036854776e+18

In [14]: a[0] - 1 == 2 ** 63
Out[14]: True

In [15]: a[0] -= 1

In [16]: a[0]
Out[16]: 9223372036854775808

In [17]: np.version.version
Out[17]: '1.24.2'

在二进制中,增加1将把最后一位从0变为1,1变为0,从1变为0将改变最后一位之前的位,这将保持向左进位,直到最左边的位从0变为1。
在unit 64中,如果你想从0减去1,数字0不能变小,所以它被视为2^65,从它减去1得到2^65-1,在二进制中是'1'* 64,在十进制中是18446744073709551615。

In [6]: a[0] - 1
Out[6]: 1.8446744073709552e+19

In [7]: a[0] - 1 == 2**64
Out[7]: True

然后,当使用Python int操作该值时,它被转换为float 1.8446744073709552e+19,由于格式的限制,实际上是2^64。

In [8]: a[0] -= 1
<ipython-input-8-9ab639258820>:1: RuntimeWarning: invalid value encountered in cast
  a[0] -= 1

In [9]: a
Out[9]: array([9223372036854775808], dtype=uint64)

现在这变得有趣了,uint 64可以保存的最大值是2^64 - 1,因为2^64在二进制中是1后跟64个0,所以它不能像uint 64中那样呈现,在这种情况下,它在递减之前被转换为0,因为2^64中的最后64位是0。
这就是为什么有一个警告。
但是在计算时,不知何故它被转换为有符号的int 64,然后再次转换为uint 64。
计算结果为-1,以带符号的int 64形式存储时为'1'+'0'*63,因为最左边的位用于符号,如果设置符号位,则数字为负。
因为一位用于符号,所以int 64的最大值是2^63-1,十进制为9223372036854775807。
当int 64中的数字-1被转换为uint 64时,它被视为2^63,即十进制的9223372036854775808,因为该数字的数值为2^63。
然后,无论我做多少次递减,数字都保持不变,因为当操作发生时,uint 64类型被转换为浮点型,其值为2^63,并且递减1不会改变该值。

ujv3wf0j

ujv3wf0j4#

可能的解决方法

1.显式强制转换

a[0] -= np.uint64(1)
++
  • 清洁
-
  • 累赘

2.花式索引

a[[0]] -= 1
+
  • 易于打字
-

3.切片索引

a[0:1] -= 1
-
  • 轻度笨重
  • 不是最快的
sd2nnvve

sd2nnvve5#

您看到的行为是由于numpy中的无符号整数运算的工作方式。当无符号整数递减时,如果结果为负,它将“回绕”到数据类型的最大值。
在您的示例中,a[0]从值0xFFFFFFFFFFFF开始,这是64位无符号整数的最大值。当您从中减去1时,结果是0xFFFFFFFFFFFFFE,正如您所期望的那样。但是,当您再次从中减去1时,结果为-1(在二进制中表示为0xFFFFFFFFFFFF)。由于此值为负,因此它会返回到数据类型的最大值,即0。
因此,您看到的行为是由于无符号整数算术的属性而预期的。如果要避免此行为,可以改用有符号整数数据类型。

相关问题