从NumPy数组创建Python bytearray时,额外的数据从哪里来?

4
考虑两种朴素方法制作相同的bytearray(使用Python 2.7.11,但在3.4.3中确认了相同的行为):
In [80]: from array import array

In [81]: import numpy as np    

In [82]: a1 = array('L',  [1, 3, 2, 5, 4])

In [83]: a2 = np.asarray([1,3,2,5,4], dtype=int)

In [84]: b1 = bytearray(a1)

In [85]: b2 = bytearray(a2)

由于array.arraynumpy.ndarray都支持缓冲区协议,因此我希望在转换为bytearray时两者都导出相同的基础数据。

但是上述数据:

In [86]: b1
Out[86]: bytearray(b'\x01\x03\x02\x05\x04')

In [87]: b2
Out[87]: bytearray(b'\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00')

一开始我以为对NumPy数组进行一个单纯的bytearray调用可能会由于数据类型、连续性或其他开销数据而无意中获取一些额外的字节。

但即使直接查看NumPy缓冲区数据句柄,它仍然显示大小为40并提供相同的数据:

In [90]: a2.data
Out[90]: <read-write buffer for 0x7fb85d60fee0, size 40, offset 0 at 0x7fb85d668fb0>

In [91]: bytearray(a2.data)
Out[91]: bytearray(b'\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00')

相同的错误也会出现在 a2.view() 中:
In [93]: bytearray(a2.view())
Out[93]: bytearray(b'\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00')

我注意到如果我给出dtype=np.int32,那么bytearray(a2)的长度为20而不是40,这表明额外的字节与类型信息有关 -- 只是不清楚为什么或如何:

In [20]: a2 = np.asarray([1,3,2,5,4], dtype=int)

In [21]: len(bytearray(a2.data))
Out[21]: 40

In [22]: a2 = np.asarray([1,3,2,5,4], dtype=np.int32)

In [23]: len(bytearray(a2.data))
Out[23]: 20

据我所知,np.int32 应该对应于 array 的类型码 'L',但任何有关为什么不是的解释都将非常有帮助。
如何可靠地提取只有 "应该" 通过缓冲区协议导出的数据部分......也就是说,在这种情况下与纯 array 数据看起来相同。

1
“应该”被导出是什么意思?缓冲区协议只是指定如何获取数据,它并未说明数据应该是什么。 - BrenBarn
这是因为numpy默认是64位(16个nibbles)吗?尝试更改字节顺序(大端,小端)并查看发生了什么。请参阅http://docs.scipy.org/doc/numpy/reference/arrays.dtypes.html。 - ralf htp
2个回答

6

当您从array.array创建字节数组时,它将其视为整数的可迭代对象,而不是缓冲区。您可以看到这一点,因为:

>>> bytearray(a1)
bytearray(b'\x01\x03\x02\x05\x04')
>>> bytearray(buffer(a1))
bytearray(b'\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00')

即,直接从数组创建一个bytearray会给你“普通”的整数,但从数组的缓冲区创建bytearray会给你这些整数的实际字节表示。另外,你不能从一个包含无法适应单个字节的整数的数组创建bytearray:

>>> bytearray(array.array(b'L', [256]))
Traceback (most recent call last):
  File "<pyshell#38>", line 1, in <module>
    bytearray(array.array(b'L', [256]))
ValueError: byte must be in range(0, 256)

行为仍然令人困惑,因为array.arraynp.ndarray都支持缓冲区协议和迭代,但是以某种方式从array.array创建一个bytearray通过迭代获取数据,而从numpy.ndarray创建一个bytearray则通过缓冲区协议获取数据。这两种类型的C内部可能有一些奇怪的解释,但我不知道是什么。
无论如何,说你在a1中看到的是“应该”发生的事情并不完全正确;就像我上面展示的那样,数据'\x01\x03\x02\x05\x04'实际上不是array.array通过缓冲区协议公开的。如果有任何问题,numpy数组的行为应该是你从缓冲区协议中得到的;array.array的行为与缓冲区协议不一致。

numpy.ndarray上进行迭代会产生数组数据类型的标量,而任何整数类型码的array.array则会产生int值,因此行为不同。 - Stop harming Monica
@Goyo 当我将int用作numpy dtype时,情况怎么样?为什么两个numpy案例的总字节长度分别为20和40?似乎不仅仅是一个简单的故事,即array.array ints只提供所需的最少字节数(即1),而numpy始终提供4个字节...这似乎并没有完全发生。 - ely
@Goyo:这仍然没有解释清楚,因为bytearray([np.int32(x) for x in 1, 2, 3])返回的bytearray仍然是带有“普通”int值的,不像bytearray(np.array([1, 2, 3], dtype=np.int32))那样。所以问题不仅仅在于个别值的问题。 - BrenBarn
1
@Mr.F:当你将int作为dtype指定时,numpy会选择平台默认的numpy整数类型,这在你的情况下显然是int64。你可以检查结果数组的dtype以查看它实际上是什么类型。 - BrenBarn
啊,是的,正确的。这也有点奇怪。我敢打赌让NumPy拥有任意精度整数作为dtype非常困难,但似乎array可以使用它。 - ely
显示剩余2条评论

5
我使用两种情况得到相同的字节数组:
In [1032]: sys.version
Out[1032]: '3.4.3 (default, Mar 26 2015, 22:07:01) \n[GCC 4.9.2]'
In [1033]: from array import array

In [1034]: a1=array('L',[1,3,2,5,4])
In [1035]: a2=np.array([1,3,2,5,4],dtype=np.int32)

In [1036]: bytearray(a1)
Out[1036]: bytearray(b'\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00')
In [1037]: bytearray(a2)
Out[1037]: bytearray(b'\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00')

无论哪种情况,我都有5个数字,每个数字占用4个字节(作为32位整数)-总共20个字节。 bytearray可能需要以下方法(或等效方法):
In [1038]: a1.tobytes()
Out[1038]: b'\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00'
In [1039]: a2.tostring()
Out[1039]: b'\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00'

我可以通过更改数据类型(dtype)来删除多余的字节:

In [1059]: a2.astype('i1').tostring()
Out[1059]: b'\x01\x03\x02\x05\x04'

https://docs.python.org/2.6/c-api/buffer.html

自从版本1.6起,Python提供了Python级别的缓冲区对象和C级别的缓冲区API,以便于任何内置或自定义类型都可以暴露其特性。然而,由于各种缺陷,它们都已被弃用,并在Python 3.0中正式移除,改用新的C级别缓冲区API和名为memoryview的新的Python级别对象。

新的缓冲区API已经向后兼容到Python 2.6,memoryview对象已经向后兼容到Python 2.7。强烈建议使用它们而不是旧的API,除非出于兼容性原因无法使用。

由于缓冲接口的这些变化,不足为奇的是,旧的array模块没有在2.6和2.7中进行更改,但在3.0+中进行了更改。


你说得对。我没有重新检查Python 3中的array.array示例,只有NumPy示例。这与在Python 3中重新实现array.array以直接支持缓冲区协议的方式有关。因此,这似乎解释了为什么bytearray在Python 2中将其视为可迭代对象。bytearray必须首先检查传递的数据是否支持直接缓冲区访问(在Python 2中array.array不支持,必须使用间接语法)。如果是,则按照您所示获取数据。如果不是,就像在Python 2中一样,然后它会退而将其视为int的可迭代对象。 - ely
然而,奇怪的是,bytearray 没有首先检查传递的数据是否支持旧式缓冲区协议,只有在 两个 缓冲区检查都失败后才会迭代值。如果您正在构建一个处理缓冲区并且还希望与 Python 2 和 Python 3 兼容的库,则整个主题将变得非常重要。 - ely
可能是开发历史的问题。 array 模块存在已经很久了,但随着 numpy 的增长,它变得有点落后。bytearray 在 2.6 版本中是新加入的。我认为缓冲区协议的概念应该属于 Python3,并且要进行一定程度的向下兼容到 Py2。在 Py3 中,默认字符串是 Unicode,bytestrings 是特殊情况。 - hpaulj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接