NumPy 用 ndarray 表示同质多维数组,并在整个数组上执行数值运算。
数据能组成规则的数值网格,而且你需要切片、聚合、线性代数或与 Python 科学计算库交换数据时使用它。
先明确每个轴的含义和 dtype,再用数组运算组合计算;遇到广播、切片或类型转换时检查结果的形状和内存所有权。
是什么,为什么存在
NumPy 是 Python 的数值数组库。它的核心对象是 多维数组(ndarray) :一组同类型元素,加上一份描述维度、数据类型和内存布局的元数据。二维数组可以表示「样本 × 特征」,三维数组可以表示「图像高度 × 宽度 × 通道」,但 NumPy 本身不会替你记录这些领域含义。
Python 列表适合保存异质对象和不断变化的集合,却没有数组级算术的统一语义。[1, 2] * 2 会重复列表,而 NumPy 数组乘以 2 会逐元素计算。NumPy 为规则数值数据提供共同的数组协议,Pandas、SciPy、Matplotlib 和许多机器学习工具因此能交换数据,而不必为每次运算编写 Python 循环。
当数据是规则、稠密、同质的数值网格时,NumPy 很合适。带列名和混合类型的表格通常更适合 Pandas 或 Polars;形状高度不规则的对象集合仍可能适合 Python 容器。这个边界很实用:不要为了「向量化」而把本来不规则的数据硬塞进 dtype=object 数组。
这里的示例已经在 Python 3.14.3 和 NumPy 2.5.2 环境中运行。显示结果经过明确的舍入或列表转换,避免依赖数组的省略规则或终端宽度。
工作原理
一个数组的三份契约
每个数组都有 形状(shape) 、 数据类型(dtype) 和数据缓冲区。shape 是各轴长度组成的元组;ndim 是轴数;size 是元素总数。dtype 决定每个元素如何解释,例如 int64、float32 或 bool。
数组变量还隐含一份领域契约:每个轴代表什么。形状同为 (32, 10) 的两个数组,可能分别表示 32 个样本的 10 个特征,也可能表示 32 个时间点的 10 个传感器。NumPy 只验证长度能否配合,不会发现轴的业务含义颠倒。
步幅(stride) 说明沿每个轴移动一个位置时跨过多少字节。切片和转置可以只改 shape 与 strides,继续引用同一缓冲区。于是两个外观不同的数组可能共享数据,这也是修改切片有时会改到原数组的原因。
创建数组时确定类型
np.array() 从现有 Python 数据创建数组,np.zeros()、np.ones() 和 np.full() 创建已初始化的规则数组,np.arange() 与 np.linspace() 创建数值序列。业务边界处应显式给出 dtype,尤其是整数宽度、浮点精度和输入可能为空时。这样可以避免类型由某一批样本偶然决定。
np.empty() 只分配空间,不初始化其中的值。它适合随后会覆盖每个元素的内部实现,不适合当作「空值数组」。如果任何位置可能在赋值前被读取,就应使用带有确定初值的创建函数。
reshape() 要求元素总数保持不变。用 -1 可以让 NumPy 推导一个轴的长度,但一条形状合法的语句仍可能表达错误的数据布局。重塑前后都要写清每个轴的意义,而不是只检查元素数能否整除。
常见形状操作的差异可以概括如下:
| 目的 | 表达式 | 结果要点 |
|---|---|---|
| 改变维度长度 | a.reshape(rows, cols) | 元素数不变,可能共享内存 |
| 交换全部轴 | a.T | 二维时交换行列,通常是视图 |
| 指定轴顺序 | a.transpose(order) | order 是输出轴对应的输入轴 |
| 移动一个轴 | np.moveaxis(a, source, destination) | 适合把通道轴移到明确位置 |
| 插入长度为一的轴 | np.expand_dims(a, axis) | 常用于准备广播 |
| 删除长度为一的轴 | np.squeeze(a, axis=axis) | 指定 axis 可避免误删其他轴 |
| 沿新轴组合 | np.stack(arrays, axis=axis) | 所有输入形状必须相同 |
| 沿已有轴拼接 | np.concatenate(arrays, axis=axis) | 非拼接轴的长度必须相同 |
移动与组合轴
转置不是「把任何数组的行列互换」。二维数组的 .T 确实交换两个轴,但三维数组的 .T 会反转全部轴顺序。处理批次、通道和空间维度时,transpose() 或 moveaxis() 能把意图写得更明确。
stack() 与 concatenate() 解决不同问题。stack() 新建一个轴,例如把十个形状为 (64, 64) 的图像组合成 (10, 64, 64);concatenate() 沿已有轴接长数组。生成代码很容易在这里多出或丢掉一个批次轴。
不带 axis 的 squeeze() 会删除所有长度为 1 的轴。这在测试数据中看似方便,却可能让批量大小为 1 时的返回值突然少一个维度。公共 API 应指定要删除的轴,或者干脆保留稳定的批次维度。
np.newaxis 就是 None 的别名,用在索引中可以插入长度为 1 的轴。values[:, np.newaxis] 把 (n,) 变成 (n, 1),values[np.newaxis, :] 则变成 (1, n)。两者元素相同,但广播方向完全不同。
组合数组前先验证除目标轴以外的长度。错误消息只能指出某些维度不匹配,不知道一个输入是否使用「通道优先」,另一个却使用「通道最后」。接口中的轴命名比修复一条 ValueError 更重要。
向量化、通用函数与聚合
向量化(vectorization) 是把计算表达为整个数组上的操作。例如,prices * quantities 表示逐元素乘法,np.sqrt(values) 把同一个通用函数应用到每个元素。向量化首先改变的是表达方式:循环由 NumPy 处理,形状与类型规则集中在数组操作中。
sum()、mean()、max() 等聚合通过 axis 指定要消去的轴。对于形状为「样本 × 特征」的二维数组,axis=0 会消去样本轴,留下每个特征的结果;axis=1 会消去特征轴,留下每个样本的结果。keepdims=True 保留长度为 1 的轴,后续广播时通常更清楚。
普通算术运算是逐元素的。两个二维数组之间的 * 不是矩阵乘法;线性代数乘法使用 @ 或 np.matmul()。np.linalg.solve() 用于求解方阵线性方程组,通常比显式计算逆矩阵再相乘更直接。
广播从尾轴开始
广播(broadcasting) 让不同形状的数组参与逐元素运算。NumPy 从最右侧轴开始比较:两个长度相等,或者其中一个长度为 1,这一对轴就兼容;缺失的左侧轴按长度 1 处理。否则运算会抛出形状不兼容错误。
例如,形状为 (4, 3) 的样本矩阵可以减去形状为 (3,) 的特征均值,因为尾轴长度都是 3。形状为 (4,) 的样本均值不能直接从它减去,因为尾轴 3 与 4 不兼容。把样本均值保留为 (4, 1) 后,长度为 1 的尾轴才会沿特征方向广播。
广播描述的是逻辑上的扩展,不等于先创建一份重复数据。不过,算术结果通常仍会分配自己的数组。先预测结果形状,再写表达式;「没有报错」只能证明形状兼容,不能证明轴的含义正确。
索引决定共享还是复制
基本切片,例如 array[1:4] 或 matrix[:, ::2],通常返回共享数据的 数组视图(array view) 。修改视图中的元素会在原数组中可见。view.base 有助于探索,但一串视图的 base 不一定直接就是手里的原数组,因此判断两个数组是否重叠应使用 np.shares_memory()。
整数数组索引和布尔索引属于高级索引,结果是副本。array[[0, 2]] 与 array[array > 0] 后续可以独立修改,但复制会分配新缓冲区。混合基本索引与高级索引时,结果形状也可能与逐轴切片的直觉不同,应该用小输入先确认。
赋值时规则不同:array[mask] = 0 会写回原数组,因为索引出现在赋值目标上。相反,先保存 selected = array[mask],再改 selected,只会修改那份副本。代码审查时需要看完整的数据流,不能只凭一个索引表达式判断最终写到了哪里。
掩码、条件与顺序
比较数组会得到布尔数组。多个条件必须使用逐元素运算符 &、| 和 ~,而且每个比较通常要放在括号中;Python 的 and 与 or 需要把整个数组解释成一个真假值,因此会抛出歧义错误。
布尔索引只保留满足条件的元素,常常会压平原有的结构。np.where(condition, left, right) 则在两个同形或可广播的值之间逐元素选择,并保留广播后的形状。选择哪一个取决于下游是否还需要原来的轴。
缺失值不只有 NaN 一种表示。浮点数组能直接保存 NaN,整数数组不能;把整数输入与 NaN 放在一起创建数组,通常会提升到浮点类型。若缺失与零、空字符串或某个哨兵值混用,聚合前必须先统一语义。
np.argsort() 返回能重排数组的索引,而不是已经排序的值。需要同步重排标签与特征时,先计算一份顺序索引,再把它应用到所有关联数组。只排序一列会破坏样本内部的对应关系。
掩码和顺序索引的长度也属于数组契约。一个来自过滤前数据的旧掩码,不能安全地复用到过滤后的数组。数据管道应让掩码与它对应的数据一起流动,或者在每个阶段重新计算。
错误与数值警告
形状不兼容通常会抛出 ValueError。先读取异常中列出的输入形状,再从尾轴向左比较,比盲目插入 reshape() 更安全。能让代码运行的形状修补不一定保留数据含义。
不安全的原地类型转换会失败。例如,浮点除法结果不能按 same_kind 规则写回整数数组。修复方向通常是创建所需浮点类型的结果,而不是强制使用会截断信息的转换。
除零、无效运算和溢出的浮点操作默认可能发出 RuntimeWarning,同时产生 inf 或 NaN。如果调用方忽略警告,这些特殊值还能继续流入后续计算,所以只看函数是否抛出异常并不够。
np.errstate() 可以在一个明确的代码块内改变浮点错误处理方式。测试关键数值路径时,可以把相关警告临时提升为异常;预期会产生特殊值的算法则可以在局部处理,并立即验证结果。
np.seterr() 改变的是当前线程的 NumPy 错误设置,不适合由普通库函数随意修改。局部上下文比隐藏的环境变化更容易推理,也不会要求调用者在函数返回后恢复设置。
np.isfinite() 能同时筛出 NaN 与正负无穷。输入验证和关键中间结果都可以使用它,但是否拒绝、替换或保留这些值仍应由领域契约决定。
警告策略不能替代边界测试。要分别覆盖除零、极大值、极小值和全缺失切片,因为它们触发的类型提升、警告和返回形状可能不同。
示例
读懂形状、类型和轴
这个二维数组的行表示日期,列表示传感器。axis=1 消去传感器轴,得到每日均值;axis=0 消去日期轴,得到每个传感器的峰值。
import numpy as np
temperatures = np.array(
[[18.5, 20.0, 19.5], [21.0, 23.5, 22.0]],
dtype=np.float64,
)
print(f"shape={temperatures.shape}")
print(f"ndim={temperatures.ndim}, dtype={temperatures.dtype}")
print("daily mean:", np.round(temperatures.mean(axis=1), 2).tolist())
print("sensor peak:", temperatures.max(axis=0).tolist())shape=(2, 3)
ndim=2, dtype=float64
daily mean: [19.33, 22.17]
sensor peak: [21.0, 23.5, 22.0]输出形状 (2, 3) 本身不能说明哪个轴是日期。变量名、邻近注释和接口文档要保存这份语义。聚合后也应检查结果形状是否与消费方的预期一致。
用广播标准化特征
下面把每列当作一个特征。keepdims=True 让均值与标准差保持 (1, 3),所以减法和除法都明确沿样本轴广播。第三列是常数,代码把它的零标准差替换为 1.0,避免除零。
import numpy as np
samples = np.array(
[[1.0, 10.0, 7.0], [2.0, 20.0, 7.0], [3.0, 30.0, 7.0]]
)
means = samples.mean(axis=0, keepdims=True)
scales = samples.std(axis=0, keepdims=True)
safe_scales = np.where(scales == 0, 1.0, scales)
standardized = (samples - means) / safe_scales
np.set_printoptions(precision=2, suppress=True)
print("mean shape:", means.shape)
print(standardized)
print("constant feature:", standardized[:, 2].tolist())mean shape: (1, 3)
[[-1.22 -1.22 0. ]
[ 0. 0. 0. ]
[ 1.22 1.22 0. ]]
constant feature: [0.0, 0.0, 0.0]把零标准差替换为 1.0 是这里明确选择的策略,不是普遍规则。另一个系统可能要删除常数特征或拒绝输入。数值代码必须把这种领域决定写进接口和测试。
区分视图与副本
基本切片 scores[:, 1:] 与原数组共享数据,布尔索引则创建副本。两次赋值看起来相似,却只有第一次回写到 scores。
import numpy as np
scores = np.array(
[[70, 80, 90], [60, 75, 85], [88, 92, 95]],
dtype=np.int64,
)
last_two = scores[:, 1:]
selected = scores[scores[:, 0] >= 70]
print("slice shares memory:", np.shares_memory(scores, last_two))
print("mask shares memory:", np.shares_memory(scores, selected))
last_two[0, 0] = 999
selected[0, 0] = -1
print("source first row:", scores[0].tolist())
print("selected first row:", selected[0].tolist())slice shares memory: True
mask shares memory: False
source first row: [70, 999, 90]
selected first row: [-1, 80, 90]需要隔离修改时,调用 .copy() 明确取得独立缓冲区。需要原地更新时,也要在函数名或文档中写明副作用。依赖调用者猜测所有权,会让一次看似局部的清洗污染上游数据。
隔离随机状态并求最小二乘解
default_rng() 返回独立的随机数生成器,不会重置模块级随机状态。这里用它生成训练与测试索引,再由 np.linalg.lstsq() 拟合含截距的一元线性模型。
import numpy as np
rng = np.random.default_rng(42)
feature = np.array([0, 1, 2, 3, 4, 5], dtype=np.float64)
target = 1.0 + 2.0 * feature
order = rng.permutation(feature.size)
train_index = order[:4]
test_index = order[4:]
design = np.column_stack(
[np.ones(train_index.size), feature[train_index]]
)
intercept, slope = np.linalg.lstsq(
design, target[train_index], rcond=None
)[0]
predicted = intercept + slope * feature[test_index]
print("train index:", train_index.tolist())
print(f"model: y = {intercept:.1f} + {slope:.1f}x")
print("test index:", test_index.tolist())
print("prediction:", np.round(predicted, 6).tolist())train index: [3, 2, 5, 4]
model: y = 1.0 + 2.0x
test index: [1, 0]
prediction: [3.0, 1.0]固定种子让这次运行可重复,但长期实验还应保存实际索引、NumPy 版本和所选位生成器。不要把「同一个种子」当作跨所有版本与算法的完整数据溯源。真实建模还要在分割前明确分层、分组和时间顺序等约束。
陷阱
修复方法: 在边界处断言形状,并为每个轴写出含义。使用 keepdims=True、np.newaxis 或明确的 reshape() 表达要扩展的轴,再用一个很小的手算样本验证结果。
修复方法: 需要隔离时显式 .copy(),需要确认重叠时调用 np.shares_memory()。对原地修改函数写测试,既检查返回值,也检查输入数组是否按契约改变。
修复方法: 在入口处选择足够宽的 dtype,用 np.result_type() 检查组合运算的结果类型。除法、归一化和累计求和前尤其要确认类型,并对最大值、最小值与负数编写边界测试。
修复方法: 只有能证明每个元素都会在读取前赋值时才用 empty()。否则使用 zeros()、full() 或一个能表达缺失策略的明确初值。
修复方法: 接收一个 Generator 参数,或在函数内部用明确种子创建局部 Generator。实验需要跨进程重放时,还要保存抽样结果和位生成器信息。
修复方法: 近似比较使用 np.isclose() 或 np.allclose(),并根据领域设置容差。先决定 NaN 应当拒绝、忽略还是传播,再选择 mean()、nanmean() 或显式验证;不要为了得到数字而默认忽略缺失值。
内存布局和数据所有权
ndarray 把数据缓冲区与解释缓冲区的元数据分开。元数据包括 shape、dtype、strides 和从缓冲区起点算起的偏移。一个视图可以更改这些字段而不复制元素,所以转置、反向切片和间隔切片仍能看到同一批底层数据。
这种设计也意味着「数组连续」不是恒真条件。C 连续数组的最后一个轴通常相邻,Fortran 连续数组的第一个轴通常相邻;转置后的数组可能只满足其中一种,也可能都不满足。调用外部库前应查看其契约,只有接口确实要求时才用 np.ascontiguousarray() 物化一份连续副本。
ravel() 会尽量返回视图,但必要时也会复制;flatten() 总是返回副本。reshape() 在布局允许时可以返回视图,否则可能复制,某些形状与顺序组合还会失败。所有权重要时,不要用「这个函数一般不会复制」代替检查和测试。
广播视图可能让多个逻辑位置指向同一个元素。np.broadcast_to() 返回只读视图,避免一次赋值含糊地影响多个位置。算术表达式的输出通常是新数组,但 out= 参数和原地运算会改变所有权与类型转换要求,使用时应在接口中写明。
内存消耗可以从数组本身核对:size * itemsize 等于元素缓冲区的 nbytes。这不包含 Python 数组对象的少量元数据,也不代表一个视图拥有同等大小的新缓冲区。排查峰值内存时,要区分视图的逻辑大小、实际拥有的缓冲区,以及表达式创建的临时结果。
类型提升和数值边界
dtype 是数组运算的一部分,不只是存储选项。两个输入参与运算时,NumPy 根据其数据类型与 Python 标量的类别确定共同结果类型;np.result_type() 可以在不执行完整计算的情况下检查该决定。赋值回现有数组还要满足转换规则,因此浮点结果不能安全地原地写回整数数组。
固定宽度整数会在边界处溢出。累计值的范围往往比单个输入大,所以计数、像素和小整数特征即使能存进 int8,它们的和也可能需要更宽的类型。应根据最终运算范围选择类型,而不是只根据原始值选择。
浮点类型需要同时考虑范围、精度和特殊值。NaN 不等于自身,正负无穷可以通过 np.isfinite() 检出,接近零的分母会放大舍入误差。容差与缺失值策略来自问题本身;NumPy 提供检查工具,但不会替你决定什么结果可以接受。
数组 API 还会区分逐元素语义与线性代数语义。a * b、a @ b 和 np.dot(a, b) 在高维输入上不一定相同。公共函数应约束接受的维度,并对非方形批次编写测试,这比依赖二维示例推断高维行为可靠。
随机数也有状态和算法边界。把 Generator 作为参数传入,可以让函数的随机状态归调用者所有,测试也能注入固定生成器。需要精确复现实验时,应保存输入、实际抽样结果、位生成器与库版本,而不只记录一个整数种子。
API 边界上的数组契约
输入验证
接收数组的公共函数应先决定它接受「任意 array-like 对象」,还是只接受 ndarray。np.asarray() 可以把列表等输入规范化为数组,并在输入已经兼容时避免不必要的复制。规范化之后再检查 ndim、shape、dtype 与有限值,错误信息才能对应统一表示。
不要只写 assert array.shape[1] == 3。普通断言可以在优化模式下移除,而且如果输入是一维数组,访问第二个轴会先抛出索引错误。应按顺序验证类型、维度、各轴长度和数值范围,并给调用者一条能修正输入的异常消息。
空数组需要单独的领域决定。某些逐元素变换可以自然返回空结果,而 max() 之类没有单位元的聚合无法从空输入得到答案。API 应说明空输入是被拒绝、返回空数组,还是使用显式的 initial 值。
可写性也是契约的一部分。把 array.flags.writeable 设为 False 可以阻止意外原地修改,但它不是安全边界,也不会冻结其他共享视图。真正需要隔离时仍要创建副本,并控制谁持有可写引用。
输出形状与所有权
函数文档应给出相对于输入的输出形状,例如「输入 (samples, features),输出 (features,)」。只写「返回均值」不足以区分全局标量、逐样本均值和逐特征均值。类型注解目前也不能替代运行时的具体轴契约。
返回视图可以避免复制,但会把输入的生命周期和可变性暴露给调用者。返回副本则提供隔离,却分配新缓冲区。两种选择都合理,关键是稳定、可见,并由测试覆盖修改输入和修改输出后的行为。
如果函数接受 out= 参数,就要验证目标形状、可写性和转换安全性。重叠的输入与输出还可能改变计算含义。除非底层操作明确支持这种重叠,不要假设原地写入与先计算完整结果再赋值等价。
标量返回值也需要留意。某些 NumPy 聚合返回 NumPy 标量,例如 np.float64,它能参与大多数 Python 运算,但序列化库可能要求原生 float 或 int。在 JSON 或数据库边界做明确转换,比在数值核心到处调用 .item() 更清楚。
保存与交换
.npy 格式保存一个数组的形状、dtype 和数据,.npz 可以把多个具名数组放进同一归档。它们适合 NumPy 工作流中的精确往返,但不是供所有语言通用的表格协议。跨系统交换前要根据消费者选择格式,而不是只考虑写出是否方便。
对象数组可能依赖 Python pickle。加载不可信 pickle 数据会执行构造对象所需的代码,因此不应把 allow_pickle=True 当作修复未知文件错误的通用开关。优先保存数值、布尔或定长字符串类型,并保持加载不可信文件时禁用 pickle。
文本格式会丢失一部分类型与形状信息。CSV 不能自然表示任意维数组,也无法完整保留浮点格式、字节序或结构化 dtype。写出文本时应同时定义列、缺失值、精度和重建形状的规则。
与 Pandas、机器学习框架或本地扩展交换数组时,要检查是否复制、是否共享可写内存,以及设备和字节序要求。名称相似的转换方法可能有不同所有权语义。最可靠的接口测试会修改交换前后的一侧,并验证另一侧是否应当变化。
5个问题 · 2 道输出预测题 · 1 道找错题