NumPy 2.x 完全指南【二十四】结构化数组
文章目录
- 1. 概述
- 2. 结构化数据类型
- 2.1 四种创建方式
- 2.1.1 元组列表
- 2.1.2 字符串格式
- 2.1.3 字段参数字典
- 2.1.4 字段名称字典
- 2.2 各种操作
- 2.2.1 查询、修改字段操作
- 2.2.2 字段标题
- 2.2.3 联合类型
- 2.2.4 赋值操作
- 2.2.5 索引操作
1. 概述
C
语言中的结构体(struct
)是一种用户自定义的复合数据类型,允许将多个不同类型的变量组合成一个逻辑单元,比如一个表示学生的结构体:
// 定义结构体
struct Student {char name[50]; // 姓名(字符串)int age; // 年龄(整数)float score; // 分数(浮点数)
};// 初始化
struct Student stu1 = {"Alice", 20, 95.5};
NumPy
中的结构化数组是一种特殊的数组类型,其每个元素是一个包含多个字段的结构体(类似于 C
语言的结构体)。每个字段可以有不同的数据类型(如整数、浮点数、字符串等),并且可以通过字段名访问具体数据。
示例,有多条学生成绩数据:
姓名 | 学号 | 数学成绩 | 物理成绩 |
---|---|---|---|
张三 | 2023001 | 85.5 | 90.0 |
李四 | 2023002 | 78.0 | 92.5 |
王五 | 2023003 | 95.0 | 88.0 |
使用 Python
序列进行表示:
scores = [('张三', 2023001, 85.5, 90.0),('李四', 2023002, 78.0, 92.5),('王五', 2023003, 95.0, 88.0)
]
之前有说过 NumPy
中大多数数组会有一些限制,比如数组的所有元素必须是相同类型(同质)的数据,而这里每条数据有字符串、整数、浮点数三种类型。直接转换为 Numpy
数组时,默认都会自动转为字符串类型:
x = np.array(scores)
print(x.dtype) # <U32:Unicode 字符串类型
print(x)
# [['张三' '2023001' '85.5' '90.0']
# ['李四' '2023002' '78.0' '92.5']
# ['王五' '2023003' '95.0' '88.0']]
这时结构化数组就派上用场了,这里使用最常用的元组列表定义结构化的数据类型对象:
# 定义方式:[(字段名, 数据类型, 形状(可选)), ...]
dtype_score = np.dtype([('name', 'U10'), # 姓名:最多 10 字符的 Unicode 字符串('student_id', 'i4'), # 学号:32 位整数('math_score', 'f4'), # 数学成绩:32 位浮点数('physics_score', 'f4') # 物理成绩:32 位浮点数
])
返回的类型是 VoidDType
:
最后,使用 np.array
直接创建就可以了:
# 创建学生数据结构化数组
students = np.array(scores, dtype=dtype_score)
print(students)
# [('张三', 2023001, 85.5, 90. )
# ('李四', 2023002, 78. , 92.5)
# ('王五', 2023003, 95. , 88. )]# 使用索引获取结构体
print(students[0]) # ('张三', 2023001, 85.5, 90.0)
可以通过使用字段名称来访问和修改结构化数组的单个字段:
# 获取所有学生名称
print(students["name"]) # ['张三' '李四' '王五']
# 获取所有学号
print(students["student_id"]) # [2023001 2023002 2023003]
# 修改所有数学成绩为 99
students["math_score"] = 99.0
print(students)
# [('张三', 2023001, 99., 90. )
# ('李四', 2023002, 99., 92.5)
# ('王五', 2023003, 99., 88. )]
需要额外注意的是,虽然数据在逻辑上类似二维表格(例如 3
行 × 3
列),但结构化数组在 NumPy
中始终是一维的,每一行的所有字段被压缩成一个结构化元素。
print(students.shape) # (3,)
注意事项:
- 结构化数组仍是
ndarray
对象,只是通过特殊的dtype
(VoidDType
)允许每个元素包含多个字段。 - 对于需要处理表格数据(如存储在
csv
文件中的数据)时,有限推荐使用xarray
、pandas
或DataArray
等更加专业的库,它们提供了一个高级接口来进行表格数据分析,并且在这方面的优化更好。例如,numpy
中的结构化数组的C
结构内存布局可能导致与缓存相关的性能问题。 - 更详细和高级用法请参考官方文档
2. 结构化数据类型
2.1 四种创建方式
可以通过 numpy.dtype
函数来创建结构化数据类型,有 4
种不同的指定形式,它们在灵活性和简洁性上有所不同。
2.1.1 元组列表
每个元组表示一个字段,其形式为 (fieldname, datatype, shape)
,Numpy
会自动确定内存布局(字节偏移量、总大小等),其中:
fieldname
:字段名称(字符串),若为空字符串''
,则自动生成默认名称f#
(例如f0
,f1
)。datatype
:字段的数据类型,可以是:- 类型字符串(如 ‘
i4
’、‘f8
’)。 NumPy
类型对象(如np.int32
、np.float64
)。- 其他可转换为数据类型的对象(如 ‘
datetime64[ns]
’)。
- 类型字符串(如 ‘
shape
(可选):字段的子数组形状(多维数组),默认为标量(无此参数)。
示例 1 ,定义包含三个字段的 dtype
:
dtype = np.dtype([('x', 'f4'), # 字段名 'x',32位浮点数(标量)('y', np.float32), # 字段名 'y',32位浮点数(同上,等效写法)('z', 'f4', (2, 2)) # 字段名 'z',2x2 的 32位浮点数组
])
示例 2 ,空字段名的自动命名:
dtype = np.dtype([('x', 'f4'), # 字段名 'x'('', 'i4'), # 空字段名,自动命名为 'f1'('z', 'i8') # 字段名 'z'
])
示例 3 ,定义包含三个字段的 dtype
:
print(students.shape) # (3,)
2.1.2 字符串格式
通过逗号分隔的字符串可以快速定义,基本格式:
<类型1>, <类型2>, ...
注意事项:
- 字段名自动生成
f0
,f1
,f2
…,无法自定义。 - 字段的字节偏移量和总大小由
NumPy
自动计算。
示例 1 ,使用字符串指定多个字段的类型:
dtype = np.dtype('i8, f4, S3')
print(dtype) # [('f0', '<i8'), ('f1', '<f4'), ('f2', 'S3')]
示例 2 ,带形状的类型:
# 3int8 表示一个长度为3的 int8 数组
# (2,3)float64 表示一个 2x3 的 float64 数组
dtype = np.dtype('3int8, float32, (2,3)float64')
print(dtype) # [('f0', 'i1', (3,)), ('f1', '<f4'), ('f2', '<f8', (2, 3))]
2.1.3 字段参数字典
通过字典定义,是最灵活的方式,允许精确控制内存布局。
字典包含以下键:
键名 | 必选/可选 | 说明 |
---|---|---|
names | 必选 | 字段名称列表(如 ['col1', 'col2'] ) |
formats | 必选 | 数据类型列表(如 ['i4', 'f4'] ),与 names 长度一致 |
offsets | 可选 | 字段的字节偏移量列表(如 [0, 4] ),未指定时自动计算 |
itemsize | 可选 | 总字节大小(如 12 ),需足够容纳所有字段(包括填充) |
aligned | 可选 | 布尔值(True /False ),是否内存对齐(类似 C 结构体) |
titles | 可选 | 字段的额外标题(别名),用于复杂元数据 |
示例 1 ,自动计算偏移量:
dtype = np.dtype({'names': ['col1', 'col2'],'formats': ['i4', 'f4']
})print(dtype) # [('col1', '<i4'), ('col2', '<f4')]
示例 2 ,手动指定偏移量和总大小:
dtype = np.dtype({··'names': ['col1', 'col2'],'formats': ['i4', 'f4'],'offsets': [0, 4], # 手动指定偏移量'itemsize': 12 # 总大小设为 12 字节(添加 4 字节填充)
})print(dtype) # {'names': ['col1', 'col2'], 'formats': ['<i4', '<f4'], 'offsets': [0, 4], 'itemsize': 12}
2.1.4 字段名称字典
字典的键是字段名称,值是指定类型和偏移量的元组:
np.dtype({'col1': ('i1', 0), 'col2': ('f4', 1)})
# dtype([('col1', 'i1'), ('col2', '<f4')])
由于 Python
字典在 Python 3.6
之前的版本不保留顺序,因此不推荐这种形式。
2.2 各种操作
2.2.1 查询、修改字段操作
示例 1 ,通过 dtype.names
属性获取字段名称的元组:
dtype_score = np.dtype([('name', 'U10'), # 姓名:最多 10 字符的 Unicode 字符串('student_id', 'i4'), # 学号:32 位整数('math_score', 'f4'), # 数学成绩:32 位浮点数('physics_score', 'f4') # 物理成绩:32 位浮点数
])
# 查看字段名称列表
print(dtype_score.names) # ('name', 'student_id', 'math_score', 'physics_score')
示例 2 ,通过字段名直接索引 dtype
对象,获取该字段的数据类型:
# 获取字段 'name' 的数据类型
print(dtype_score['name']) # 输出:<U10# 获取字段 'student_id' 的数据类型
print(dtype_score['student_id']) # 输出:int32
示例 3 ,通过赋值 dtype.names
修改字段名称(需谨慎,可能破坏现有代码逻辑):
# 将字段名 'name' 改为 'a','student_id' 改为 'b','math_score' 改为 'c','student_id' 改为 'physics_score'
dtype_score.names = ('a', 'b', 'c', 'd')
print(dtype_score.names) # 输出:('a', 'b', 'c', 'd')
示例 4 ,通过 dtype.fields
属性获取类似字典的结构,包含字段的数据类型和偏移量:
# 查看字段的详细信息
print(dtype_score.fields)
# 输出:{'name': (dtype('<U10'), 0), 'student_id': (dtype('int32'), 40), 'math_score': (dtype('float32'), 44), 'physics_score': (dtype('float32'), 48)}
2.2.2 字段标题
标题可作为字段的别名或描述性名称,增强数据可读性。
示例 1 ,元组列表形式:
# 定义带标题的 dtype
dtype = np.dtype([(('我的标题', 'name'), 'f4'), # 标题为 '我的标题',字段名为 'name'('age', 'i4')
])
print(dtype) # [(('我的标题', 'name'), '<f4'), ('age', '<i4')]# 通过字段名访问
print(arr['name']) # 输出:['Alice']# 通过标题访问(需直接使用标题字符串)
print(arr['我的标题']) # 输出:['Alice'](标题需是 ASCII 兼容字符串,否则可能报错)
示例 2 ,字典形式:
dtype = np.dtype({'names': ['name', 'age'], # 字段名列表'formats': ['f4', 'i4'], # 数据类型列表'titles': ['姓名', '年龄'] # 标题列表(与 names 长度一致)
})print(dtype) # [(('姓名', 'name'), '<f4'), (('年龄', 'age'), '<i4')]
2.2.3 联合类型
联合类型允许不同字段共享同一块内存空间,类似于 C
语言中的 union
。这意味着不同字段的数据可能覆盖同一内存区域,修改一个字段会影响其他字段的值。
示例:
import numpy as np# 定义一个联合类型:共享 4 字节内存
dtype_union = np.dtype((np.int32, { # 基础类型为 int32(4 字节)'names': ['i', 'f'], # 两个字段'formats': ['i2', 'f2'], # 各占 2 字节(共享 4 字节)'offsets': [0, 2] # 字段偏移量
}))# 创建联合类型数组
arr = np.array([(1, 2.0)], dtype=dtype_union)
print(arr['i']) # 输出:[1]
print(arr['f']) # 输出:[2.0]# 修改一个字段会影响另一个字段
arr['i'] = 0x4048F5C3 # 将 int32 的二进制数据解释为 float32
print(arr['f']) # 输出:[3.14](假设小端字节序)
2.2.4 赋值操作
示例 1 ,使用 Python
元组按字段顺序赋值:
# 定义结构化数组
dtype = np.dtype([('name', 'U10'), ('age', 'i4'), ('score', 'f4')])
students = np.empty(3, dtype=dtype)# 用元组赋值
students[0] = ('Alice', 25, 88.5) # 直接赋值整个元素
students[1:] = [('Bob', 30, 92.3), ('Charlie', 28, 76.9)]
示例 2 ,赋值给结构化元素的标量将被分配给所有字段:
x = np.zeros(2, dtype='i8, f4, ?, S1')
x[:] = 3
print(x) # [(3, 3., True, b'3') (3, 3., True, b'3')]
示例 3 ,将非结构化数组赋值给结构化数组时,非结构化数组的最后一个维度的大小必须等于结构化数组的字段数量:
x = np.zeros(2, dtype='i8, f4, ?, S1') # shapr:(2,)
print(x) # [(0, 0., False, b'') (0, 0., False, b'')]x[:] = [1, 2] # shapr:(2,)
print(x) # [(1, 1., True, b'1') (2, 2., True, b'2')]
2.2.5 索引操作
示例 1 ,结构化数组的单个字段可以通过字段名称索引访问和修改::
x = np.array([(1, 2),(3, 4)],dtype=[('foo', 'i8'), ('bar', 'f4')])
# 访问
print(x['foo']) # [1 3]
# 修改
x['foo'] = 10
print(x) # [(10, 2.) (10, 4.)]
示例 2 ,结果数组是原始数组的视图,写入视图会修改原始数组:
y = x['foo']
y[:] = 20
print(x) # [(20, 2.) (20, 4.)]