复合数据类型
结构化数组
前面介绍过,在创建数组时使用 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')
})
 
 
还可以忽略字段名称,仅用一个逗号分隔的字符串来指定复合类型:
    
    
    
    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[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() 函数读取文本数据。不过文本形式无法保存复杂的结构化数组数据,因此不建议使用。