【问题标题】:When creating a Python bytearray from NumPy array, where does the extra data come from?从 NumPy 数组创建 Python 字节数组时,额外的数据从何而来?
【发布时间】:2016-03-06 22:27:42
【问题描述】:

考虑两种天真地创建相同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' 类型代码,但任何关于为什么不的解释都会有很大帮助。

如何才能可靠地仅提取“应该”通过缓冲区协议导出的数据部分......就像在这种情况下,与普通 array 数据一样。

【问题讨论】:

  • “应该”被导出是什么意思?缓冲区协议只是指定如何获取数据,并没有说明数据应该是什么。
  • 这是因为 numpy 默认为 64 位(16 个半字节)吗?尝试改变字节顺序(大端,小端),看看会发生什么。看到这个docs.scipy.org/doc/numpy/reference/arrays.dtypes.html

标签: python numpy buffer bytearray protocol-buffers


【解决方案1】:

当您从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(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 都支持 缓冲区协议 迭代,但不知何故从@ 创建了一个字节数组987654326@ 通过迭代获取数据,而从numpy.ndarray 创建一个字节数组通过缓冲区协议获取数据。对于这两种类型的 C 内部结构中的这种切换优先级,大概有一些神秘的解释,但我不知道它是什么。

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

【讨论】:

  • 迭代 numpy.ndarray 会产生数组 dtype 的标量,而任何整数类型代码的 array.array 会产生 int 值,因此会有不同的行为。
  • 我的意思是一种行为或其他行为应该是“预期的”方式。至少令人惊讶的是,这两种类型对 bytearray 的简单调用不同是不好的。如果这两种行为中的任何一种是预期的默认值,我会很高兴。在我的应用程序中,这只是意味着在处理array.array 时,我需要小心不要陷入一堆buffer 调用。
  • @Goyo 当我使用int 作为 numpy dtype 时的情况如何......为什么在两个 numpy 情况下总字节长度分别为 20 和 40。似乎更多的事情正在发生,这只是一个简单的故事,array.array int 根据需要提供尽可能少的字节(即 1),而 numpy 总是提供 4 个字节......这似乎并没有发生。
  • @Goyo:这仍然无法解释,因为bytearray([np.int32(x) for x in 1, 2, 3]) 仍然返回一个带有“普通”int 值的字节数组,这与bytearray(np.array([1, 2, 3], dtype=np.int32)) 不同。所以这不仅仅是个人价值观的问题。
  • @Mr.F:当您指定 int 作为 dtype 时,numpy 只会选择平台默认的 numpy 整数类型,在您的情况下显然是 int64。您可以检查结果数组的 dtype 以查看其 dtype 实际是什么。
【解决方案2】:

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

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 必须首先检查传递的数据是否支持直接缓冲区访问(array.array 在 Python 2 中不支持,您必须使用间接成语)。如果是这样,它会按照您显示的方式获取数据。如果它没有,就像在 Python 2 中一样,那么它无法将其视为 ints 的可迭代对象。
  • 奇怪的是,bytearray 不会首先检查传递的数据是否支持旧式缓冲区协议,只有在 both 缓冲区检查之后才会检查诉诸于迭代值。如果您正在构建一个处理缓冲区并同时寻求与 Python 2 和 Python 3 兼容的库,那么整个主题变得非常重要。
  • 这可能是发展历史的问题。 array 模块一直存在,但随着 numpy 的发展,它在某种程度上是发展的死水。 bytearray 是 2.6 中的新功能。而且我认为缓冲区协议的概念属于 Python3,对 Py2 有一定的反向移植。在 Py3 中,默认字符串是 unicode,bytestrings 是特殊情况。
猜你喜欢
  • 2017-01-19
  • 2021-06-29
  • 2013-11-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-25
  • 2023-01-04
  • 2017-12-30
相关资源
最近更新 更多