A.5 结构化和记录式数组

你可能已经注意到了,到目前为止我们所讨论的ndarray都是一种同质数据容器,也就是说,在它所表示的内存块中,各元素占用的字节数相同(具体根据dtype而定)。从表面上看,它似乎不能用于表示异质或表格型的数据。结构化数组是一种特殊的ndarray,其中的各个元素可以被看做C语言中的结构体(struct,这就是“结构化”的由来)或SQL表中带有多个命名字段的行:

  1. In [144]: dtype = [('x', np.float64), ('y', np.int32)]
  2. In [145]: sarr = np.array([(1.5, 6), (np.pi, -2)], dtype=dtype)
  3. In [146]: sarr
  4. Out[146]:
  5. array([( 1.5 , 6), ( 3.1416, -2)],
  6. dtype=[('x', '<f8'), ('y', '<i4')])

定义结构化dtype(请参考NumPy的在线文档)的方式有很多。最典型的办法是元组列表,各元组的格式为(field_name,field_data_type)。这样,数组的元素就成了元组式的对象,该对象中各个元素可以像字典那样进行访问:

  1. In [147]: sarr[0]
  2. Out[147]: ( 1.5, 6)
  3. In [148]: sarr[0]['y']
  4. Out[148]: 6

字段名保存在dtype.names属性中。在访问结构化数组的某个字段时,返回的是该数据的视图,所以不会发生数据复制:

  1. In [149]: sarr['x']
  2. Out[149]: array([ 1.5 , 3.1416])

嵌套dtype和多维字段

在定义结构化dtype时,你可以再设置一个形状(可以是一个整数,也可以是一个元组):

  1. In [150]: dtype = [('x', np.int64, 3), ('y', np.int32)]
  2. In [151]: arr = np.zeros(4, dtype=dtype)
  3. In [152]: arr
  4. Out[152]:
  5. array([([0, 0, 0], 0), ([0, 0, 0], 0), ([0, 0, 0], 0), ([0, 0, 0], 0)],
  6. dtype=[('x', '<i8', (3,)), ('y', '<i4')])

在这种情况下,各个记录的x字段所表示的是一个长度为3的数组:

  1. In [153]: arr[0]['x']
  2. Out[153]: array([0, 0, 0])

这样,访问arr[‘x’]即可得到一个二维数组,而不是前面那个例子中的一维数组:

  1. In [154]: arr['x']
  2. Out[154]:
  3. array([[0, 0, 0],
  4. [0, 0, 0],
  5. [0, 0, 0],
  6. [0, 0, 0]])

这就使你能用单个数组的内存块存放复杂的嵌套结构。你还可以嵌套dtype,作出更复杂的结构。下面是一个简单的例子:

  1. In [155]: dtype = [('x', [('a', 'f8'), ('b', 'f4')]), ('y', np.int32)]
  2. In [156]: data = np.array([((1, 2), 5), ((3, 4), 6)], dtype=dtype)
  3. In [157]: data['x']
  4. Out[157]:
  5. array([( 1., 2.), ( 3., 4.)],
  6. dtype=[('a', '<f8'), ('b', '<f4')])
  7. In [158]: data['y']
  8. Out[158]: array([5, 6], dtype=int32)
  9. In [159]: data['x']['a']
  10. Out[159]: array([ 1., 3.])

pandas的DataFrame并不直接支持该功能,但它的分层索引机制跟这个差不多。

为什么要用结构化数组

跟pandas的DataFrame相比,NumPy的结构化数组是一种相对较低级的工具。它可以将单个内存块解释为带有任意复杂嵌套列的表格型结构。由于数组中的每个元素在内存中都被表示为固定的字节数,所以结构化数组能够提供非常快速高效的磁盘数据读写(包括内存映像)、网络传输等功能。

结构化数组的另一个常见用法是,将数据文件写成定长记录字节流,这是C和C++代码中常见的数据序列化手段(业界许多历史系统中都能找得到)。只要知道文件的格式(记录的大小、元素的顺序、字节数以及数据类型等),就可以用np.fromfile将数据读入内存。这种用法超出了本书的范围,知道这点就可以了。