将 c_char_p_Array 加载到 Numpy 数组中。

4
考虑使用以下代码创建一个有N个元素的C缓冲区:
from ctypes import byref, c_double

N = 3
buffer = (c_double * N)()
# C++ function that fills the buffer byref
pull_function(byref(buffer))
# load buffer in numpy
data = np.frombuffer(buffer, dtype=c_double)

非常好用。但我的问题是 dtype 可能是数字(float、double、int8等)或字符串。

from ctypes import byref, c_char_p

N = 3
buffer = (c_char_p * N)()
# C++ function that fills the buffer byref
pull_function(byref(buffer))
# Load in.. a list?
data = [v.decode("utf-8") for v in buffer]

我怎样才能将那些UTF-8编码的字符串直接加载到numpy数组中呢?np.char.decode似乎是一个不错的选择,但我无法弄清楚如何使用它。np.char.decode(np.frombuffer(buffer, dtype=np.bytes_))出现了错误:ValueError: itemsize cannot be zero in type

编辑:缓冲区可以从Python API中填充。对应的代码行为:

x = [list of strings]
x = [v.encode("utf-8") for v in x]
buffer = (c_char_p * N)(*x)
push_function(byref(buffer))

请注意,这是与上面不同的缓冲区。`push_function`将`x`中的数据推送到网络上,而`pull_function`从网络中检索数据。两者都是LabStreamingLayer C++库的一部分。 编辑2:如果我可以在将其发送到网络之前重新加载“push”缓冲区到numpy数组中,我认为我可以使其工作。'pull'缓冲区可能是相同的。因此,这里有一个演示上述`ValueError`的MWE。
from ctypes import c_char_p

import numpy as np


x = ["1", "23"]
x = [elt.encode("utf-8") for elt in x]
buffer = (c_char_p * 2)(*x)
np.frombuffer(buffer, dtype=np.bytes_)  # fails
[elt.decode("utf-8") for elt in buffer]  # works

你能修改 C 函数吗?每个字符串有最大长度吗? - Hack5
@Hack5 你好,我无法控制C函数。它似乎没有最大长度限制。我尝试了100k元素的字符串,并且缓冲区填充/加载正常工作。Python API可以通过调用另一个C函数来填充缓冲区。我在文章中添加了相应的代码行。 - Mathieu
我认为你需要使用纯Python或C来解析字符串。Numpy似乎期望在长度为(字符串长度*字符串数量)的缓冲区中具有固定的最大长度字符串。一旦你将它们转换成这种形式,就可以通过np.dtype("a" + str(total_length))轻松地将它们导入到np中。 - Hack5
@Hack5 但是numpy支持具有可变大小字符串的数组,对吧?np.array(["1", "123"])的dtype为<U3 - Mathieu
@BillHorvath 我不知道如何调试,所以无法回答你的问题,请查看第二次编辑,我尝试重新加载推送到网络上的缓冲区(假设与我尝试从网络中拉取的缓冲区完全相同)。至少这样,问题是可以重现的。 - Mathieu
显示剩余2条评论
2个回答

2
您可以使用ctypesstring_at将字节缓冲区转换为Python字符串。使用buffer.decode("utf-8")也可以像您看到的那样工作(只适用于一个c_char_p,而不是它们的数组)。 c_char_p * N是字符指针的数组(基本上是C字符串的数组,具有C类型char*[3])。重点是Numpy使用平坦缓冲区存储字符串,因此几乎必须进行复制。Numpy数组中的所有字符串都具有有限大小,整个数组的保留大小为arr.size * maxStrSize * bytePerChar,其中maxStrSize是数组的最大字符串,除非手动更改/指定,bytePerChar对于Numpy字节字符串数组(即S)为1,对于Numpy Unicode字符串数组(即U)通常为4。实际上,Numpy应该使用UCS-4编码来表示Unicode字符串(据我所知,根据Python解释器的编译方式,Unicode字符串也可以在内存中表示为UCS-2,但可以通过检查np.dtype('U1').itemsize == 4是否为真来检查是否使用了UCS-4编码)。不进行复制的唯一方法是,如果您的C++代码可以直接写入预分配的Numpy数组。这意味着C++代码必须使用与Numpy数组相同的表示,并且在调用C++函数之前已知所有字符串的有限大小。 np.frombuffer将缓冲区解释为一维数组。因此,缓冲区需要是平坦的,而您的缓冲区不是,因此在这种情况下无法直接使用np.frombuffer
一个相当低效的解决方案是简单地将字符串转换为CPython字节数组,然后构建一个包含所有字符串的Numpy数组,因此Numpy将找到最大的字符串,分配大缓冲区并复制每个字符串。这很容易实现:np.array([elt.decode("utf-8") for elt in buffer])。这不是非常有效,因为CPython会转换每个字符串并分配字符串,然后由Numpy读取再被释放。
更快的解决方案是将每个字符串复制到原始缓冲区中,然后使用np.frombuffer。但实际操作起来并不简单:需要使用strlen(或已知边界大小的情况下),检查字符串的大小,然后分配一个大缓冲区,接着使用memcpy循环(如果字符串小于最大大小,则不应忘记在最后写入0字符),最后使用np.frombuffer(通过指定dtype='S%d' % maxLen)。这当然可以在Cython或使用C扩展程序中完成,以提高性能。更好的替代方案是预先分配一个Numpy数组,并直接在其原始缓冲区中编写。但有一个问题:这仅适用于ASCII /字节字符串数组(即S),而不适用于Unicode字符串(即U)。对于Unicode字符串,需要从UTF-8编码中解码字符串,然后将其重新编码为UCS-2 / UCS-4字节缓冲区。由于@BillHorvath指出了零大小的dtype,因此无法在此情况下使用np.frombuffer。因此,需要手动执行该操作,因为据我所知只有使用快速专用库的C才能以有效的方式执行此操作。请注意,Unicode字符串往往固有低效(因为每个字符的大小不同),因此如果目标字符串保证为ASCII,则请考虑使用字节字符串。

1
非常感谢您的详细解释。至少目前,我会保留使用列表推导式进行转换,因为它比np.array([elt.decode("utf-8") for elt in buffer])更简单和更快。 - Mathieu

1
看起来你看到的错误信息是因为 bytes_ 是一种灵活的数据类型,其默认情况下 itemsize 为0

24个内置的数组标量类型对象都转换为相关联的数据类型对象。对于它们的子类也是如此...请注意,并非所有数据类型信息都可以使用类型对象提供:例如,灵活的数据类型默认的itemsize为0,需要明确给出大小才能有用。

使用默认情况下大小为0的dtype从缓冲区重构数组是设计上的失败
如果你预先知道缓冲区中将要看到的数据的类型和长度,则 这个答案可能会给你想要的解决方案

自定义 dtype 的一种方法是在 getnfromtxt 中分配 names,然后使用 astype 重新转换值。


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