numpy.empty函数的dtype参数是如何工作的?

apeeds0o  于 2023-10-19  发布在  其他
关注(0)|答案(2)|浏览(91)

当我不包括dtype参数时,我想我大概理解了这个机制。假设我写:

代码块1

import numpy as np

test=np.empty((1,2))

print(test)

输出量:

[[-2.00000000e+000  6.80176343e-310]]

我明白这里发生了什么。操作系统将一个(连续的)内存块分配给数组test。该测试只取内存块单元中已经存在的任意值。好极了!
现在,让我们稍微修改一下代码:
代码块2**

import numpy as np

test=np.empty((1,2), dtype=int)

print(test)

输出
[[4611686018427387904 137669224398847]]
我的问题是,操作系统是如何找到一个只包含整数的内存块的?这看起来很神奇我的猜测是,实际上操作系统找到了一个包含float 64数字的内存,但只是截断了小数点后的所有内容,使其成为整数。我猜的对吗?我还有一个问题即使我多次运行代码块1和2,输出也不会改变。真奇怪我期望输出随着每次迭代而改变。我认为操作系统会在每次迭代时将数组“test”分配给不同的内存块。看起来好像操作系统一遍又一遍地将数组“test”分配给同一个内存块。奇怪。只有当我改变形状时输出才改变。你能解释一下输出不改变的原因吗?

kxeu7u2r

kxeu7u2r1#

我的问题是,操作系统是如何找到一个只包含整数的内存块的?
它不一定是以前用于整数的内存。
它找到了一个内存块,不管之前有什么,它都把它们重新解释为整数。
这里有一个重新解释记忆的例子。我把0到9作为浮点数。然后,我使用ndarray.view()将它们重新解释为整数。

import numpy as np
a = np.arange(10, dtype='float64')
print(a)
b = a.view('int64')
print(b)

输出量:

[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
[                  0 4607182418800017408 4611686018427387904
 4613937818241073152 4616189618054758400 4617315517961601024
 4618441417868443648 4619567317775286272 4620693217682128896
 4621256167635550208]

当你把浮点数中构成1的位重新解释为整数时,你会得到一个完全不同的数字。
我还有一个问题即使我多次运行代码块1和2,输出也不会改变。
内存分配器可能会返回您刚刚释放的同一块内存。这是完美的尺寸,毕竟,你不再使用它了。
这里有一个例子可以证明这一点。我分配一个空数组。我来决定地址。我把它添加到一个集合中,以找出数组中存储了多少个唯一地址。然后我循环。我只保留对最近的数组a的引用。

import numpy as np
pointers = set()
for i in range(100):
    a = np.empty(10)
    pointer, read_only_flag = a.__array_interface__['data']
    pointers.add(pointer)
    print(pointer)
print(f"{len(pointers)=}")

您可能会认为这会导致一百个不同的地址。但内存分配器比这更聪明:它可以告诉我,除了一个数组之外,所有我分配的数组都不再被引用,它可以重用这些内存。它只显示两个唯一指针。
许多使用NumPy的程序会随着时间的推移分配和释放许多NumPy数组。(例如,如果ab是数组,则a = a + b分配并释放一个数组。)如果内存分配器不能重用已释放的内存,这将是一种浪费。
另一种可能的解释是,这个相同的模式在内存中出现了多次,这是Python正在做的其他事情。没有足够的信息来说明哪一个正在发生。

nom7f22z

nom7f22z2#

记忆不是这样运作的。
好吧,在解释型语言(如Python)中,每个数据(每个值)都伴随着一个描述符,给出了它的类型。所以int或者float已经是一个相当复杂的结构了,你不希望随机找到它们。
Numpy比Python更像C。它是Python绑定到一个主要是C库。
因此,np.empty所做的只是malloc的C等价物。它找到了一个记忆的地方,而不去关心它是什么。并分配它。请注意,这个内存内容将被解释为一堆给定dtype的数据。
例如,np.empty((1,2))查找1×2×8字节(需要存储1行2个float 64类型的数据)= 16字节的内存。malloc(16)也是。从现在开始,这个内存被解释为float64
几乎所有随机字节都是浮点数(如果不是所有的位组合都有意义,那将是浪费位。这意味着我们不能像用字节那样表示尽可能多的值)。
所以,不管这16个字节是什么,你都可以把它们解释为2个浮点数(如果你不能,那么它们就是NaN。所以它仍然是东西)
在您的示例中,当numpy决定将它们用于test数组时,内存中已经存在的16个字节恰好是

[0, 0, 0, 0, 0, 0, 0, 192, 22, 148, 56, 157, 53, 125, 0, 0]

所以解释为float 64,前8个字节是(0,0,0,0,0,0,192)。我们必须倒过来读。即(192,0,0,0,0,0,0)。这是二进制的

11000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

如果您选择将这64位解释为表示浮点数,则第一位为1,因此它是负数。接下来的11位是1,后面是10个0。所以,指数是1000000000,或者十进制的1024。由于浮点数64中的指数是相对于1023的(1023是指数0,1024是指数1,1025是指数2,1022是指数-1,1021是指数-2,...),所以这里的指数是1。
那么剩下的52位都是0。Float 64表示意味着在这52位之前有一个隐式1。所以尾数是1后面跟着52个0。也就是说1+0/2+0/2²+0/2 <$+...+0/2 <$² = 1。
所以1乘以2的指数1 = 1×2 = 2。-2,因为指数是负数。
同样,接下来的8个字节,代表第二个浮点数是[0,0,125,53,157,56,148,22](反向,因为又是小端)。
以二进制

00000000 00000000 01111101 00110101 10011101 00111000 10010100 00010110

符号是0
指数为0000000000 = 0。这是一个特殊情况(一个次正常数),所以指数是-1022。尾数是0 0000 01111101 00110101 10011101 00111000 10010100 00010110(包括次正常数的隐式0)。
所以

(0/2**0 + 0/2**1 + 0/2**2 + 0/2**3 + 0/2**4 + 0/2**5 + 1/2**6 + 1/2**7 + 1/2**8 + 1/2**9 + 1/2**10 + 0/2**11 + 1/2**12 + 0/2**13 + 0/2**14 + 1/2**15 + 1/2**16 + 0/2**17 + 1/2**18 + 0/2**19 + 1/2**20 + 1/2**21 + 0/2**22 + 0/2**23 + 1/2**24 + 1/2**25 + 1/2**26 + 0/2**27 + 1/2**28 + 0/2**29 + 0/2**30 + 1/2**31 + 1/2**32 + 1/2**33 + 0/2**34 + 0/2**35 + 0/2**36 + 1/2**37 + 0/2**38 + 0/2**39 + 1/2**40 + 0/2**41 + 1/2**42 + 0/2**43 + 0/2**44 + 0/2**45 + 0/2**46 + 0/2**47 + 1/2**48 + 0/2**49 + 1/2**50 + 1/2**51 + 0/2**52)*2**-1022

正如预期的那样,它是6.80176343e-310
现在,如果你分配了一个int 32的2x2数组,也就是2x2 x4 =16字节,并且碰巧得到了完全相同的字节,那么你的数组将是

array([[         0, 3221225472],
       [2637730838,      32053]], dtype=uint32)

这是由字节表示(在小端):

0, 0, 0, 0
3221225472%256, (3221225472//2**8)%256, (3221225472//2**16)%256, (3221225472//2**24)
2637730838%256, (2637730838//2**8)%256, (2637730838//2**16)%256, (2637730838//2**24)
32053%256, (32053//2**8)%256, (32053//2**16)%256, (32053//2**24)

所以0, 0, 0, 0, 0, 0, 0, 192, 22, 148, 56, 157, 53, 125, 0, 0
相同的字节。
所以,这就是整件事。任何一组字节都可以被解释为uint16int32float64。除了“bytes”之外,没有其他类型。
您可以使用frombuffertobytes比我更容易地“玩”这种方式

test=np.array([[-2.00000000e+000, 6.80176343e-310]])
# Another array using the same memory, but another type and shape
t2=np.frombuffer(test, dtype=np.uint8).reshape(2,8)
t2
# array([[  0,   0,   0,   0,   0,   0,   0, 192],
#       [ 22, 148,  56, 157,  53, 125,   0,   0]], dtype=uint8)
# Let's change the first bit of the first float of `test` 
# That is the first bit of the last bytes of 1st row of t2 (because, little endian: first byte of 
# each number is the last)
t2[0,7]=64
# That is the sign bit of the first float of test. So , now test[0,0] should be +2 instead of -2
test
# array([[2.00000000e+000, 6.80176343e-310]])
# yep, it works

# or increase exponent by 1. That means that the last bit of exponent turn from 0 to 1. That is 12th bit of the 64 bits of the float
# So 4th bit (from the leftmost) of 2nd byte (so `t2[0,6]`) 
# Now it is 0. If 4th bit were 1, it would be 16.
# So 
t2[0,6]=16
# Normally test[0,0] should now be twice bigger, so 4
test
# array([[4.00000000e+000, 6.80176343e-310]])
# Yep, works again

嗯,我被带走了。最后一个游戏和你的问题有很大的关系。但它告诉你:相同的字节,2种方式来解释它们。
这就是np.empty的情况。它找到一些字节,而不试图从它们的内容或类型中选择它们(同样,没有类型。它们只是字节)。
然后,它决定将then解释为float 64(就像你对test所做的那样)或uint8(就像我对t2所做的那样)或任何需要的东西。

相关问题