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道找错题