numpy06:高级数据类型

复合数据类型

结构化数组

前面介绍过,在创建数组时使用 dtype 参数可以指定数组元素的类型:

np.zeros(4, dtype=np.complex128)
array([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j])

numpy 提供了许多数据类型,可以满足各种场景的使用。但是这种方式创建的数组只能容纳同种类型的元素。

通过在列表内安排多个数据类型,可以指定复合数据类型,构造一个结构化数组:

students = np.empty(3, dtype=[
    ('name', np.unicode_, 16),
    ('age', np.int16),
    ('score', np.float_)
])
students.dtype
dtype([('name', '<U16'), ('age', '<i2'), ('score', '<f8')])

这种复合数据类型可以通过字段名来区分各个数据成员,就像命名元组一样。

当检查该数组的类型时,可以看到复合数据类型由多个简单数据类型组合而成。这里基本数据类型以简写形式表达,U16 表示“长度不超过 16 的 Unicode 字符串”,i2 表示“ 2 字节(16 位)整型”,f8 表示“ 8 字节(64 位)浮点型”。最前方的小于号 < 表示以低字节序(little endian)存储。

更多字符对应的 numpy 的数据类型如下表所示:

数据类型符号描述 数据类型符号描述
'b'字节型 'c'浮点型复数
'i'有符号整型 'S', 'a'字符串
'u'无符号整型 'U'Unicode 字符串
'f'浮点型 'V'原生数据

除了元组列表外,还可以通过字典的形式表达复合数据类型:

students = np.zeros(3, dtype={
    'names': ('name', 'age', 'score'),
    'formats': ('<U16', '<i2', '<f8')
})

还可以忽略字段名称,仅用一个逗号分隔的字符串来指定复合类型:

np.dtype('S16,i2,f8')
dtype([('f0', 'S16'), ('f1', '<i2'), ('f2', '<f8')])

这种通过 dtype 类构造的类型也可以直接用作传参。

现在生成了一个空的数组容器,可以将列表数据放入数组中:

students['name'] = ['Alice', 'Tim', 'Duff']
students['age'] = [25, 19, 31]
students['score'] = [75.0, 61.3, 57.9]
students
array([('Alice', 25, 75. ), ('Tim', 19, 61.3), ('Duff', 31, 57.9)], dtype=[('name', '<U16'), ('age', '<i2'), ('score', '<f8')])

结构化数组所有的数据被安排在一个内存块中,因此它的存储和运算效率都优于元组列表。

可以通过索引或名称查看相应的值。例如,以下获取结构数组所有 name 字段的内容:

students['name']  # name field
array(['Alice', 'Tim', 'Duff'], dtype='<U16')

以下获取数据的第一个结构:

students[0]  # first struct
('Alice', 25, 75.)

以下获取最后一个结构的 name 字段:

students[-1]['name']
'Duff'

利用布尔数组数据,还可以做一些更复杂的操作,如按照分数筛选结构,再提取结构字段:

students[students['score'] > 60]['name']
array(['Alice', 'Tim'], dtype='<U16')

更高级的复合类型

numpy 中也可以定义更高级的复合数据类型,其中每个字段又是一个复合数据类型。

例如,可以创建一种结构,其中每个元素都包含一个数组或矩阵:

im3_3 = np.dtype(
    [('id', 'i8'), ('matrix', 'f8', (3, 3))]
)
c1 = np.zeros(5, dtype=im3_3)
c1[0]
(0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])

使用第三个参数可以确定数组的形状。

使用这种规则还可以创建嵌套结构类型,例如:

classroom = np.dtype([
    ('teacher', np.dtype([
        ('name', np.unicode_, 16),
        ('age', np.int16)
    ])),
    ('students', student, 3)
])
grade_7 = np.empty(8, dtype=classroom)
grade_7[0]['teacher']['name'] = 'Mr.C'
grade_7[0]['teacher']['age'] = 32
grade_7[0]['students']['name'] = ['Alice', 'Tim', 'Duff']
grade_7[0]['students']['age'] = [25, 19, 31]
grade_7[0]['students']['score'] = [75.0, 61.3, 57.9]
grade_7[0]
(('Mr.C', 32), [('Alice', 25, 75. ), ('Tim', 19, 61.3), ('Duff', 31, 57.9)])

它的使用方法和普通的数组并没有太大差异。

numpy 还提供了 recarray 类。它和前面介绍的结构化数组几乎相同,但是它有一个独特的特征:字段可以像属性一样获取,而不是像字典的键那样获取。

例如,以下使用 .view() 方法,将一个数组通过视图表现为 recarray 类。这种类型的转换不修改底层数据,只改变数据的处理方式:

graderec_7 = grade_7.view(np.recarray)
graderec_7[0].students[1].name
'Tim'

NumPy与I/O

在处理大量数据时,将结果暂时保存到磁盘上是有必要的。numpy 提供了一系列 I/O 操作,可以与磁盘交互。

最基本的 save(file, arr, allow_pickle=True, fix_imports=True) 函数将数组保存到扩展名为 .npy 的文件中。allow_pickle 参数使用序列化形式保存数组对象。fix_imports 参数则是为了处理 Python2 与 Python3 的版本差异:

np.save('grade7', grade_7)

然后同级目录下就会得到一个 grade7.npy 的文件。由于对其做了序列化操作,因此该文件是二进制形式的,不能通过一般的文本编辑器打开。

读取则使用 load() 函数。如果不想使用二进制的形式保存数据,可以调用 savetxt() 以简单的文本形式存储数据,对应的使用 loadtxt() 函数读取文本数据。不过文本形式无法保存复杂的结构化数组数据,因此不建议使用。

京ICP备2021034974号
contact me by hello@frozencandles.fun