如何有效地将原始字节写入 python 中的 numpy 数组数据 3

How to efficiently write raw bytes to numpy array data in python 3

在将一些旧的 python 2 代码迁移到 python 3 时,我 运行 在从字节对象填充结构化 numpy 数组时遇到了一些问题。

我有一个解析器,它为我可能遇到的每种数据结构定义了一个特定的数据类型。由于通常给定的数据结构可能具有可变长度或可变类型的字段,因此这些字段在 numpy 数组中表示为对象 dtype (np.object #alternatively np.dtype('O')).

的字段

数组是通过首先填充固定数据类型字段从字节(或bytearray)获得的。在此之后,任何子数组(包含在 'object' 字段中)的 dtype 都可以使用它之前的固定字段中的信息来构建。

这是在 python 2 中工作的此过程的部分示例(仅处理固定数据类型的字段)。请注意,我们有一个名为 'nSamples' 的字段,它可能会告诉us 数组的 'samples' 字段指向的数组的长度,这将被解释为具有形状 (2,) 和 dtype sampleDtype:

的 numpy 数组
fancyDtype = np.dtype([('blah',     '<u4'),
                       ('bleh',      'S5'),
                       ('nSamples', '<u8'),
                       ('samples',    'O')])

sampleDtype = np.dtype([('sampleId', '<u2'),
                        ('val',      '<f4')])

bytesFromFile = bytearray(
    b'*\x00\x00\x00hello\x02\x00\x00\x00\x00\x00\x00\x00\xd0\xb5'
    b'\x14_\xa1\x7f\x00\x00"\x00\x00\x00\x80?]\x00\x00\x00\xa0@')

arr = np.zeros((1,), dtype=fancyDtype)
numBytesFixedPortion = 17

# Start out by just reading the fixed-type portion of the array
arr.data[:numBytesFixedPortion] = bytesFromFile[:numBytesFixedPortion]
memoryview(arr.data)[:numBytesFixedPortion] = bytesFromFile[:numBytesFixedPortion]

此处的最后两个语句都适用于 python 2.7.

值得注意的是,如果我键入

arr.data

我得到 <read-write buffer for 0x7f7a93bb7080, size 25, offset 0 at 0x7f7a9339cf70>,它告诉我这是一个缓冲区。显然,memoryview(arr.data) returns一个memoryview对象。

这两个语句在 python 3.6 中引发以下异常:

NotImplementedError: memoryview: unsupported format T{I:blah:5s:bleh:=Q:nSamples:O:samples:}

这告诉我 numpy 正在返回一个不同的类型及其 data 属性访问,memoryview 而不是 buffer。它还告诉我 memoryviews 在 python 2.7 中工作,但在 python 3.6 中不工作。

我在 numpy 的问题跟踪器中发现了类似的问题:https://github.com/numpy/numpy/issues/13617 然而,这个问题很快就被关闭了,numpy 开发人员指出这是 ctypes 中的一个错误。由于 ctypes 是内置的,我有点放弃了更新它以获得修复的希望。

我终于偶然发现了一个可行的解决方案,尽管它花费的时间大约是 python 2.7 方法的两倍。它是:

import struct
struct.pack_into(
    'B' * numBytesFixedPortion,  # fmt
    arr.data,                    # buffer
    0,                           # offset
    *buf[:numBytesFixedPortion]  # unpacked byte values
)

一位同事还建议尝试使用此解决方案:

arrView = arr.view('u1')
arrView[:numBytesFixedPortion] = buf[:numBytesFixedPortion]

但是,在执行此操作时,出现异常:

File "/home/tintedFrantic/anaconda2/envs/py3/lib/python3.6/site-packages/numpy/core/_internal.py", line 461, in _view_is_safe
    raise TypeError("Cannot change data-type for object array.")
TypeError: Cannot change data-type for object array.

请注意,我在 python 2.7 和 3.6 中都遇到了这个异常。看来 numpy 不允许查看具有任何 object 字段的数组。 (旁白:我能够通过在 numpy 代码中注释掉对对象类型字段的检查来让 numpy 正确地执行此操作,尽管这似乎是一个危险的解决方案(而且也不是一个非常便携的解决方案))。

我还尝试创建单独的数组,一个具有固定数据类型字段,一个具有对象数据类型字段,然后使用 numpy.lib.recfunctions.merge_arrays 合并它们。失败并显示一条我不记得的神秘消息。

我有点不知所措。我只想将一些任意字节写入 numpy 数组的底层内存并高效地执行。这看起来不应该太难做,但我还没有遇到一个好的方法来做到这一点。我想要一个也不是 hack 的解决方案,因为这将进入需要高可靠性的系统。如果没有更好的方法,我将使用 struct.pack_into() 解决方案,但我希望有人知道更好的方法。顺便说一句,不使用 object-dtype 字段不是一个可行的选择,因为这样做的成本太高了。

如果重要的话,我在 python 2.7 中使用 numpy 1.16.2,在 python 3.6 中使用 1.17.4。

根据@nawsleahcimnoraa 的建议,我发现在 python 3.3+(所以在 python 2.7 中不存在)中,memoryview 对象由 arr.data 在我的 python 3 环境中,有一个 cast() 方法。这样,我可以做到

arr.data.cast('B')[startIdx:endIdx] = buf[:numBytes]

这更像我在 python 2.7 中的情况。它比上面的 struct 方法更简洁,性能也更好。

我在测试这些解决方案时注意到的一件事是,一般来说,python 3 解决方案比 python 2 版本慢。例如,我尝试使用 python 2 和 python 3 的 struct 解决方案,发现 python 3 的处理时间显着增加。

我还发现同一版本的不同 python 环境之间存在相当大的差异。例如,我发现我的 python 3.6 系统安装比 python 3.6 的虚拟环境安装执行得更好,所以看起来结果可能在很大程度上取决于给定环境的配置。

总的来说,我对使用 arr.data 返回的 memoryview 对象的 cast() 方法的结果很满意,现在将使用它。但是,如果有人发现更好用的东西,我仍然很乐意听到。