完全嵌套的NumPy结构化标量

4
在NumPy文档和其他StackOverflow问题中,提到了嵌套的NumPy结构化标量。在我看到的所有内容中,它们似乎将嵌套的结构化标量描述为包含另一个标量(显然),但内部标量始终是另一种dtype。我想做的是创建一个NumPy dtype,其中一个字段是它自己的dtype。一个简单的例子是表示树节点的dtype,其中它会存储一些值(如整数)和表示其父节点的另一个树节点。
似乎应该使用numpy.void来实现这一点,但我无法使用以下dtype来实现:
node_dtype = np.dtype([("parent", np.void), ("info", np.uint8)])

混合类型实际上是非常好文档化并且超级棒的。 - Sam Ragusa
我在发布了那个评论后检查了“structured-array”标签,发现自己有一个很大的盲点。谢谢 :) - roganjosh
3个回答

2

np.void

我猜你认为np.void能够工作,因为结构化数组记录的类型是void

In [32]: node_dtype = np.dtype([("parent", np.void), ("info", np.uint8)])
In [33]: np.zeros(3, node_dtype)
Out[33]: 
array([(b'', 0), (b'', 0), (b'', 0)],
      dtype=[('parent', 'V'), ('info', 'u1')])
In [34]: type(_[0])
Out[34]: numpy.void

但请注意
In [35]: __['parent']
Out[35]: array([b'', b'', b''], dtype='|V0')

该字段占用0字节。

In [36]: np.zeros(3, np.void)
Out[36]: array([b'', b'', b''], dtype='|V0')
In [37]: np.zeros(3, np.void(0))
Out[37]: array([b'', b'', b''], dtype='|V0')
In [38]: np.zeros(3, np.void(5))
Out[38]: 
array([b'\x00\x00\x00\x00\x00', b'\x00\x00\x00\x00\x00',
       b'\x00\x00\x00\x00\x00'], dtype='|V5')
In [39]: _[0] = b'12345'

np.void 通常需要一个参数,即指定长度的整数。

虽然可以嵌套 dtypes,但结果必须仍然有已知的 itemsize

In [57]: dt0 = np.dtype('i,f')
In [58]: dt1 = np.dtype([('f0','U3'), ('nested',dt0)])
In [59]: dt1
Out[59]: dtype([('f0', '<U3'), ('nested', [('f0', '<i4'), ('f1', '<f4')])])
In [60]: dt1.itemsize
Out[60]: 20

生成的数组将具有已知大小的数据缓冲区,只能容纳arr.sizearr.itemsize字节的项目。

对象数据类型

您可以使用object数据类型字段构建结构化数组。

In [61]: arr = np.empty(3, 'O,i')
In [62]: arr
Out[62]: 
array([(None, 0), (None, 0), (None, 0)],
      dtype=[('f0', 'O'), ('f1', '<i4')])
In [63]: arr[1]['f0']=arr[0]
In [64]: arr[2]['f0']=arr[1]
In [65]: arr
Out[65]: 
array([(None, 0), ((None, 0), 0), (((None, 0), 0), 0)],
      dtype=[('f0', 'O'), ('f1', '<i4')])
In [66]: arr[0]['f1']=100
In [67]: arr
Out[67]: 
array([(None, 100), ((None, 100),   0), (((None, 100), 0),   0)],
      dtype=[('f0', 'O'), ('f1', '<i4')])
In [68]: arr[1]['f1']=200
In [69]: arr[2]['f1']=300
In [70]: arr
Out[70]: 
array([(None, 100), ((None, 100), 200), (((None, 100), 200), 300)],
      dtype=[('f0', 'O'), ('f1', '<i4')])

我不确定这种结构是否特别有用。一份列表可能同样好。
In [71]: arr.tolist()
Out[71]: [(None, 100), ((None, 100), 200), (((None, 100), 200), 300)]

谢谢你的精彩解释,我从未想过使用np.object类型作为结构化数组字段!如果你好奇,我使用这个结构来表示Numba编译的国际象棋引擎的游戏树。除了在结构标量中保存节点信息(非对象字段)之外,它只会用于检查或更新其父级的值,并且这将始终批量完成。 - Sam Ragusa

1
尝试这个代码导致了numpy崩溃:

>>> import numpy as np
>>>
# normal compound dtype, no prob
>>> L = [('f1', int), ('f2', float), ('f3', 'U4')]
>>> np.dtype(L)
dtype([('f1', '<i8'), ('f2', '<f8'), ('f3', '<U4')])
>>> 
# dtype containing itself
>>> L.append(('f4', L))
>>> L
[('f1', <class 'int'>), ('f2', <class 'float'>), ('f3', 'U4'), ('f4', [...])]
>>> np.dtype(L)
Speicherzugriffsfehler (Speicherabzug geschrieben)
# and that is German for segfault (core dumped)

考虑到解释这个结构的概念问题,更不用说自动为其设计内存布局了。虽然它不工作并不令人惊讶,但显然不应该崩溃。

1
最让我惊讶的是,用德语说"segfault (core dumped)"需要多少个字母。 - Sam Ragusa

1

我禁不住要尝试@hpaulj的非常巧妙的解决方案。

有一件事情让我感到困扰,我觉得知道这个对大家很有用。

它不能正常工作,或者至少在批量处理时不能正常工作:

>>> import numpy as np
>>> 
>>> arr = np.empty(4, 'O,i')
>>> arr['f1'] = np.arange(4)
>>> 
# assign one by one:
# ------------------
>>> for i in range(4): arr[i]['f0'] = arr[(i+1) % 4]
... 
# inddividual elements link up nicely:
>>> arr[0]['f0']['f0'] is arr[1]['f0']
True
>>> print([(a['f1'], a['f0']['f1'], a['f0']['f0']['f1']) for a in arr])
[(0, 1, 2), (1, 2, 3), (2, 3, 0), (3, 0, 1)]
# but don't try it in bulk:
>>> print(arr['f1'], arr['f0']['f1'])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices
>>> 
>>> arr = np.empty(4, 'O,i')
>>> arr['f1'] = np.arange(4)
>>> 
# assign in bulk:
# ---------------
>>> arr['f0'][[3,0,1,2]] = arr
>>> 
# no linking up:
>>> arr[0]['f0']['f0'] is arr[1]['f0']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: tuple indices must be integers or slices, not str
>>> print([(a['f1'], a['f0']['f1'], a['f0']['f0']['f1']) for a in arr])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <listcomp>
TypeError: tuple indices must be integers or slices, not str

链接是关键问题,我相信文档中有提到这里,同时似乎你遇到的错误来自于NumPy不喜欢你的索引方式。看起来你可以使用整数而不是字符串来索引嵌套标量以避免出现错误。另外尝试设置arr[0]['f0]=arr[3]和arr[1]['f0]=arr[3],然后更改arr[0]['f0'][1]的值。虽然“is”所有权测试失败了,但是值会按照你想象的方式更新。 - Sam Ragusa
我认为我的使用情况实际上不会受到这个限制的影响!由于任何节点的父节点都永远不会改变,因此只有其中包含的值会改变。 - Sam Ragusa
@SamRagusa 那个文档部分是个好发现!不过,两种情况之间的区别是真实存在的:逐个赋值时,你可以做出荒谬的事情,比如 arr[0]['f0']['f0']['f0']['f0']['f0']['f0']['f0']['f0']['f0'] is arr[0]['f0'] -> True,而在批量赋值的情况下,即使使用整数索引,我们也总是有限深度:arr[0][0][0] -> Nonearr[0][0][0][0] -> 错误。- 但如果您的用例没有受到影响,那就更好了。 - Paul Panzer
你指的是哪个错误?你甚至可以执行 b=arr[0]b[0]=b,然后无限循环增加相同的节点值。我一直遇到的错误是 RecursionError,似乎只在打印数组或标量时发生,而不是在使用它们时发生。 - Sam Ragusa
@SamRagusa 我的意思是第二种情况,即从一个新的空数组arr = np.empty(4, 'O,i')开始,arr['f1'] = range(4),然后进行数组赋值arr['f0'][[3,0,1,2]] = arr。你仍然可以打印这个arr,因为虽然它的元素是嵌套的,但只嵌套了一层。这也是为什么arr[0][0][0][0]不起作用的原因。你可以重复执行arr['f0'][[3,0,1,2]] = arr;每次都会添加一层嵌套。相比之下,for i in range(4): arr[i]['f0'] = arr[(i+1) % 4]因为它创建了真正的引用,所以会立即创建一个无限循环。 - Paul Panzer

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