NumPy 现代基础:数组、索引、广播与向量化
NumPy 的核心是 ndarray:带有固定数据类型和形状的多维数组。真正值得掌握的不是函数清单,而是 shape、dtype、axis、广播和内存共享这几个心智模型。本文以当前 NumPy 2.x 的写法重新整理这些基础。
NumPy 是什么
NumPy 是数值计算生态中的基础库,主要用于大型、多维数组上的运算。它的优势可以概括为:
- 使用连续、结构化的数组表达数据;
- 通过向量化运算减少重复循环;
- 为统计、科学计算和数据处理工具提供统一的数组基础。
安装基础依赖:
1 | python -m pip install numpy |
SciPy 是建立在 NumPy 之上的科学计算库,需要线性代数、优化、信号处理等扩展功能时再单独安装即可。
先记住四个属性
1 | import numpy as np |
| 属性或概念 | 要回答的问题 |
|---|---|
shape |
每个轴有多长? |
dtype |
每个元素怎样编码、占多少空间? |
axis |
当前操作沿哪个维度归约或变换? |
| 视图与副本 | 新数组是否与原数组共享底层数据? |
1. 多维数组创建
1.1 创建一维数组
1 | import numpy as np |
方式1: np.array([1,2,3,4,5,6],dtype=np.int64) – 结果[1 2 3 4 5 6] 数组a
方式2: np.array(range(1,6)) – 结果[1 2 3 4 5]
方式3: np.arange(1,6,1) – 结果[1 2 3 4 5]
方式4: np.array(list) 将一个列表或序列转化为数组
方式 5:
np.arange(a, b, c)按起点、终点和步长生成数组。
更推荐把不同创建方式直接写成可运行代码:
1 | values = np.array([1, 2, 3, 4], dtype=np.int64) |
arange 适合整数步长;需要指定区间内均匀取若干个浮点数时,linspace 通常更直观。
1.2 创建多维数组
- 二维数组 单个维度的元素数量要保持一致,数组的元素类型要保持一致(不一致则自动被转换为一致的),否则报错
1 | num_list2 = [[1,2,3,4,5],[6,7,8,9,10]] |
- 三维数组(三维矩阵) : 列表维度和要创建的数组维度一致; 数据和维度如果是灵活的情况下创建复杂,
1 | num_list4=[ [[1,2,3,4,"a"],[6,7,8,9,"b"]] , [[1,2,3,4,"a"],[6,7,8,9,"b"]] ] |
- PS:
创建多维数组时可以 - 1、创建一个全0的数组: np.zeros((3,4))
- 2、创建一个全1的数组:np.ones((3,4))
- 3、创建一个对角线为1的正方形数组(方阵):np.eye(3)
- 4、随机数创建数组,见下一部分 (6)numpy生成随机数
1.3 数组的属性
a.ndim #数组维度
a.shape #数组形状
a.itemsize #数组每个元素大小
2. 常用数据类型
2.1 NumPy 支持的数值类型
| 符号 | 含义 |
|---|---|
| bool_ | True 和 False |
| int_ | 平台原生整数宽度 |
| int8 | 8位的整形(-128~127) |
| int16 | -32768~32767 |
| int32 | -2 ** 31 ~ 2 ** 31 - 1 |
| int64 | -2 ** 63 ~ 2 ** 63 - 1 |
| uint8 | 8位的整形(0~255) |
| uint16 | 0~65535 |
| uint32 | 0 ~ 2 ** 32 - 1 |
| uint64 | 0 ~ 2 ** 64 - 1 |
| float16 | 1位符号位,5位指数位,10位 |
| float32 | 1位符号位,8位指数位,23位 |
| float64 | 1位符号位,11位指数位,52位 |
数据要写入文件或跨平台交换时,优先明确使用 int32、int64、float32 或 float64,不要依赖平台原生整数宽度。可用 np.dtype("int32") 构造 dtype,并通过 .itemsize 查看字节数。NumPy 2.x 已移除一批模糊的旧别名,新代码应使用明确的标量类型。
2.2 查看多维数组的数据类型
arr1.dtype
定义bool类型
bs = np.array([1, 1, 0, 1, 0], dtype=bool)
2.3 数据类型转换
数组名.astype(numpy.数据类型),返回数组, 需要接收后才会改变
astype 返回转换后的新数组,默认不修改原数组。浮点数转整数会截断小数部分;窄整数还可能溢出,因此正式转换前应确认取值范围。
1 | arr1 = np.array([1.2, 2.8, 3.5], dtype=np.float64) |
3. 多维数组形状
查看数组的形状
语法: a.shape
修改数组的形状
方法1: a.reshape(5,2) – 5是行, 2是列
方法2: a.resize (3,4) –表示将arr数组修改成3行4列数组,原数组a形状改变
输出 a.shape 问题: 为何修改后,仍然是原来的形状?使用b接收
把数组转为1维数组
方法1:a.reshape(1,10)
方法2:a.flatten()
方法3:a.reshape(a.shape[0] * a.shape[1], )
方法4: a.ravel()
1 | a = np.array([[1,2,3,4,5],[6,7,8,9,10]]) |
reshape 返回形状不同的数组对象,原数组的 shape 不会随之改变;新对象在条件允许时可能与原数组共享内存。flatten() 总是创建副本,ravel() 则会尽可能返回视图。
PS:多维数组的轴(维度)
在numpy中轴(axis)可以理解为方向,使用0,1,2…数字表示,
对于一个一维数组,只有一个0轴, 对于2维数组(shape(2,2)),有0轴和1轴, 对于三维数组(shape(2,2, 3)),有0,1,2轴
4. 数组计算与向量化
数组与标量运算时,标量会广播到每个元素;形状相同的数组则逐元素运算。与 Python 循环相比,数组表达式通常更简洁,也更容易交给底层已优化的实现执行。
1 | a = np.arange(1, 7).reshape(2, 3) |
axis 表示被归约掉的轴,而不是“结果朝哪个方向”。例如 (2, 3) 数组执行 sum(axis=0) 后,第 0 轴被压缩,结果形状是 (3,)。
5. 广播原则
NumPy 从两个数组 shape 的最右侧开始逐维比较。每一维只要满足以下任一条件,就可以广播:
- 两个维度长度相等;
- 其中一个维度长度为 1;
- 某一方缺少该维度,可视为长度 1。
1 | scores = np.array([[80, 90, 70], [75, 88, 92]]) # (2, 3) |
判断练习:(3, 3, 3) 与 (3, 2) 不兼容,因为末尾的 3 和 2 冲突;(3, 3, 2) 与 (3, 2) 兼容,结果为 (3, 3, 2)。
从最右侧判断广播
选择两个 shape。每一列表示对齐后的同一维,缺少的前导维会按 1 处理。
广播通常不会真的复制输入数组,但表达式产生的中间结果仍可能很大。例如给 (100000, 1) 与 (1, 100000) 做运算会得到一个百亿元素数组,形状兼容并不代表内存安全。
6. 多维数组的轴
对于形状为 (2, 3, 4) 的三维数组:第 0 轴长度为 2,第 1 轴长度为 3,第 2 轴长度为 4。不要把轴固定记成“横轴”或“纵轴”,应结合具体数据含义命名,例如“样本、时间、特征”。
7. 索引、切片与布尔筛选
1 | a = np.arange(12).reshape(3, 4) |
布尔数组还可用于原位修改,np.where 则适合生成一个新数组。判断缺失值应使用 np.isnan,不要写 x == np.nan,因为 NaN 按定义不等于自身。
1 | b = a.copy() |
切片是视图,高级索引是副本
基本切片通常返回视图,与原数组共享数据;整数数组或布尔数组构成的高级索引会返回副本。这是最容易造成隐蔽错误的规则之一。
1 | a = np.arange(6) |
如果代码依赖独立数据,请显式调用 .copy();如果依赖共享内存,可用 np.shares_memory(a, view) 检查。
常用索引写法
(1) 一维数组的索引与切片
1.获取指定元素 语法:数组名[索引(下标)] a[3]
2.数组的切片 语法:数组名[start : end : step] a[2:10:2]
(2) 二维数组的索引与切片
1. 获取指定元素 语法:数组名[行索引,列索引] a[1 , 1]
2. 数组的切片 语法:数组名[rows_start:rows_end:rows_step,cols_start:cols_end:cols_step]
a[1 : 2 , 0: 1]
1 | 取一行 a[1] |
(3)NumPy 中数值的修改
1 | #数值的修改 |
如果希望将小于 10 的数字替换为 0,可以使用布尔索引:
(4)NumPy 中布尔索引
1 | #小于10的替换为0 |
8. NumPy 转置和轴对换
转置和轴置换用于重新排列数组的轴。对二维数组来说,转置相当于交换行、列两个轴;对更高维数组,需要明确指定完整的轴顺序或要交换的两个轴。
2.方法1: a.transpose() 行和列互换 ,原来的数组从2行3列,变为3行2列.
3.方法2: a.swapaxes(1,0) 交换轴
4.方法3: a.T
5. 更高维的轴移动可使用 np.moveaxis(arr, source, destination),语义通常比旧式 rollaxis 更清楚。
转换和交换轴的效果一样.
1 | a 是二维数组 |
9. NumPy 常用方法
(1)数组的连接
1.concatenate连接NumPy数组
连接意味着将两个或多个数组的内容放在单个数组中。
我们传递了一系列要与轴一起加入concatenate()函数的数组。 如果未显式传递轴,则将其视为0。
例如:
连接两个数组
1 | import numpy as np |
上面代码运行的成果:[4 2 3 4 7 6]
例如:
沿列方向(axis=1)联接两个二维数组:
1 | arr1 = np.array([[1, 7], [3, 4]]) |
上面代码运行的成果:
1 | [[1 7 5 9] |
2.使用stack()函数连接数组
stack()与concatenate()相同,唯一的不同是stack()是沿着新轴完成的。
我们可以沿着第二个轴连接两个一维数组,这将导致它们一个放在另一个之上。
我们传递一个要连接到stack()方法的数组序列和axis。如果axis没有显式传递,则将其视为0。
例如:
1 | import numpy as np |
上面代码运行的成果:
1 | [[1 4] |
3. 使用 hstack() 水平堆叠
对二维数组而言,hstack() 沿列方向拼接,要求各数组的行数兼容。
例如:
1 | import numpy as np |
上面代码运行的成果:
1 | [ 1 77 31 4 88 61] |
4. 使用 vstack() 垂直堆叠
vstack() 沿第 0 轴拼接,相当于把数组按行叠放。
例如:
1 | import numpy as np |
上面代码运行的成果:
1 | [[11 12 3] |
5. 使用 dstack 沿第三个轴堆叠
np.dstack 把输入数组沿第三个轴(axis=2)依次堆叠。对一维输入,它会先把每个数组变成形状 (1, N, 1)。
例如:
1 | import numpy as np |
上面代码运行的成果:
1 | [[[1 4] |
(2)数组的分割
1. split()函数
沿特定的轴将数组分割为子数组
语法: numpy.split(arr,indices_or_sections,axis)
indices_or_sections表示将arr数组创建为大小相同的子数组的数量
1 | #对1维数组分割 按指定位置分割,2,5位置 |
2.hsplit()函数
将数组沿着水平方向分割
语法:numpy.hsplit(arr,indices_or_sections)
3.vsplit()函数
将数组沿着竖直方向分割
语法: numpy.vsplit(arr,indices_or_sections)
1 | #hsplit()函数 沿着水平方向分割 结果3个数组 [[1],[4]] [[2],[5]] [[3],[6]] |
(3)数组转换
数组名.tolist() 将数组转换成列表
(4)添加删除数组元素
1. append()函数
数组的末尾添加元素,该函数会返回一个新数组,而原数组不变。
方法:numpy.append(arr,values,axis)
1 | a = np.array([1, 2, 3, 4, 5, 6]) |
1 | #沿0轴添加运行结果 |
2.insert()函数
沿给定轴在输入数组中插入值,该函数会返回一个新数组,原数组不变。
方法:numpy.insert(arr,obj,values,axis)
1 | #一维数组插入 |
1 | #二维数组插入 会被展开后插入运行结果 |
3.delete()函数
从输入数组中删除指定子数组的新数组,原数组不变。
方法:numpy.delete(arr,obj,axis)
1 | # delete() 返回删除指定索引后的新数组;不传 axis 时会先展开 |
10. 通用函数
(1)常见的一元函数
| 函数 | 说明 |
|---|---|
| abs、fabs | 计算绝对值;fabs 只处理实数,复数应使用 abs |
| sqrt | 计算各元素的平方根,相当于arr ** 0.5 |
| square | 计算个元素的平方, |
| exp | 计算各元素的指数 |
| log、log10、log2、log1p | 分别计算自然对数、常用对数、以 2 为底的对数,以及 log(1+x) |
| sign | 计算各元素的符号:1(正数)、0(零)、-1(负数) |
| ceil | 计算各元素的ceiling值,即大于等于该值的最小整数 |
| floor | 计算各元素的floor值,即小于等于该值的最大整数 |
| rint | 将各元素的值四舍五入到最接近的整数,保留dtype |
| modf | 将数组各元素的小数和整数部分以两个独立的数组的形式返回 |
| isnan | 返回一个表示“哪些值是NaN(Not a Number)”的布尔型数组 |
| isfinite、isinf | 分别返回一个表示“哪些元素是有穷的”或“哪些元素是无穷的”的布尔型数组 |
| cos、cosh、sin、sinh、tan、tanh | 普通型和双曲型三角函数 |
| arccos、arccosh、arcsin、arcsinh、arctan、arctanh | 普通型和双曲型反三角函数 |
| logical_not | 计算各元素not x的真值 |
1 | #通用函数 |
(2)常见的二元函数
| 函数 | 说明 |
|---|---|
| add | 将数组中对应的元素相加 |
| subtract | 第一个数组减去第二个数组 |
| multiply | 数组元素相乘 |
| divide、floor_divide | 除法、向下整除法(丢弃余数) |
| power | 逐元素计算第一个数组的值对第二个数组的值的幂 |
| maximum、fmax | 元素级的最大值计算,fmax将忽略NaN |
| minimum、fmin | 元素级的最小值计算,fmin将忽略NaN |
| mod | 元素级的取模计算 |
| copysign | 将第二个数组各元素值的符号复制给第一个数组中各元素 |
| greater、greater_equal、less、less_equal、equal、not_equal | 执行元素级的比较运算,最终产生布尔型数组 |
| logical_and、logical_or、logical_xor | 执行元素级真值逻辑运算 |
11. 线性代数:使用 ndarray 与 @
NumPy 2.0 已移除 np.mat,新代码统一使用二维 ndarray。这样不会出现 * 在数组与矩阵对象上含义不同的问题:* 始终是逐元素乘法,@ 或 np.matmul 才是矩阵乘法。
1 | a = np.array([[1.0, 2.0], [3.0, 4.0]]) |
多个小数组拼成分块矩阵时,可使用 np.block:
1 | left = np.eye(2) |
求解线性方程 Ax = b 时,优先使用 np.linalg.solve,通常不要显式计算逆矩阵:
1 | a = np.array([[3.0, 1.0], [1.0, 2.0]]) |
常用入口还包括 np.linalg.norm(范数)、np.linalg.det(行列式)、np.linalg.eig(特征值)和 np.linalg.svd(奇异值分解)。计算前应先检查形状,并留意病态矩阵带来的数值误差。
12. 随机数生成
新代码推荐先创建 Generator,再从同一个生成器取样。这比依赖全局 np.random 状态更容易测试,也方便显式控制可复现性。
1 | rng = np.random.default_rng(seed=2026) |
固定种子适合教程、测试和可复现实验,但不代表随机结果“更随机”。正式模拟中应记录种子与 NumPy 版本;密码、令牌等安全用途则应使用 Python 的 secrets 模块,而不是 NumPy 随机数生成器。
13. 一段完整的向量化示例
假设每行是一名学生、每列是一门课程,需要做标准化并求每名学生的平均标准分:
1 | scores = np.array([ |
这里同时用到了轴、广播和向量化:keepdims=True 保留被归约轴为长度 1,使 (3, 3) 可以直接与 (1, 3) 的均值、标准差广播。真实数据还要先处理缺失值与标准差为 0 的列。
14. 学习检查清单
- 能否只看
shape判断两个数组是否可广播? - 能否说清
axis=0被压缩的是哪一维? - 修改切片后,是否知道原数组会不会变化?
- 是否区分
*的逐元素乘法与@的矩阵乘法? - 是否用
default_rng管理随机状态? - 面对循环时,能否先尝试通用函数、广播或归约?
掌握这些问题后,再学习 pandas、SciPy、scikit-learn 或深度学习框架时,数组的形状与计算语义会顺畅得多。