从 NumPy 数组创建 Python bytearray 时,额外数据从何而来?

When creating a Python bytearray from NumPy array, where does the extra data come from?

考虑两种天真地制作相同的方法 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

AFAICT,np.int32 应该对应于 array 'L' 类型代码,但任何关于为什么不这样做的解释都会非常有帮助。

如何才能可靠地仅提取 "should" 通过缓冲区协议导出的数据部分...就像这里的普通 array 数据一样案例.

当您从 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')

也就是说,直接从数组创建字节数组可以得到 "plain" 整数,但是从数组缓冲区创建字节数组可以得到这些整数的实际字节表示。此外,您不能从具有不适合单个字节的整数的数组创建字节数组:

>>> 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 都支持 both 缓冲协议 and 迭代,但以某种方式从 array.array 创建字节数组通过迭代获取数据,而从 numpy.ndarray 创建字节数组通过缓冲区协议获取数据。这两种类型的 C 内部结构中可能对这种切换优先级有一些神秘的解释,但我不知道它是什么。

无论如何,说您在 a1 上看到的就是 "should" 发生的事情是不正确的;如上所示,数据 '\x01\x03\x02\x05\x04' 实际上并不是 array.array 通过缓冲协议公开的内容。如果有的话,numpy 数组的行为就是你 "should" 从缓冲协议中得到的; array.array 行为不符合缓冲区协议。

两种情况下我得到相同的字节数组:

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'

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

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

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

Starting from version 1.6, Python has been providing Python-level buffer objects and a C-level buffer API so that any built-in or used-defined type can expose its characteristics. Both, however, have been deprecated because of various shortcomings, and have been officially removed in Python 3.0 in favour of a new C-level buffer API and a new Python-level object named memoryview.

The new buffer API has been backported to Python 2.6, and the memoryview object has been backported to Python 2.7. It is strongly advised to use them rather than the old APIs, unless you are blocked from doing so for compatibility reasons.

鉴于缓冲区接口中的这​​些变化,较旧的 array 模块在 2.6 和 2.7 中没有更改,但在 3.0+ 中更改也就不足为奇了。