文档
NumPy 入门教程 —— 从 Python 列表到向量化思维
本章目标
- 理解 NumPy 在 Python 生态系统中的定位
- 掌握 ndarray 的内存模型与性能原理
- 学会用向量化思维替代显式循环
- 掌握常见数据操作技巧
1. NumPy 的定位
Python 科学计算栈层级:
应用层: scikit-learn | TensorFlow/PyTorch | SciPy
↓ ↓ ↓
中间层: Pandas | NumPy (ndarray) | Matplotlib
↓ ↓ ↓
底层: NumPy (C/Fortran 实现 + Python API)
↓
BLAS/LAPACK (MKL / OpenBLAS / Accelerate)
一句话:NumPy 是 Python 数据科学的事实标准"通用语言"。
2. ndarray 内存模型
为什么 ndarray 这么快?
# Python 列表:每个元素是 PyObject 指针
py_list = [1, 2, 3, 4]
# 内存布局: [obj_ptr] → [int_obj 1]
# [obj_ptr] → [int_obj 2] # 内存碎片化
# ...
# NumPy ndarray:连续内存块
np_arr = np.array([1, 2, 3, 4], dtype=np.int64)
# 内存布局: [1][2][3][4] # 连续 32 bytes
三大性能来源:
- 连续内存 — CPU 缓存友好,SIMD 向量指令
- C 循环 — 运算代码在 C 层执行,绕过 Python 解释器
- BLAS — 线性代数调用高度优化的 BLAS 库(MKL/OpenBLAS)
数据排布:C-order vs Fortran-order
arr = np.arange(12).reshape(3, 4)
# C-order(默认,按行存储)
print(arr.flags['C_CONTIGUOUS']) # True
# 内存: [0 1 2 3 | 4 5 6 7 | 8 9 10 11]
# Fortran-order(按列存储)
arr_f = np.asfortranarray(arr)
# 内存: [0 4 8 | 1 5 9 | 2 6 10 | 3 7 11]
# 性能影响:沿连续维度的操作更快
3. 向量化思维
从"循环思维"到"向量化思维"
# 问题:计算 1 到 100 万每个数的 sin 值
# ❌ Python 循环思维
import math
result = [math.sin(i) for i in range(1, 1_000_001)]
# ✅ NumPy 向量化思维
x = np.arange(1, 1_000_001)
result = np.sin(x)
常见向量化模式
# ① 条件赋值
# ❌ 循环
for i in range(len(data)):
if data[i] > 0:
data[i] = 1
else:
data[i] = -1
# ✅ 向量化
data = np.where(data > 0, 1, -1)
# ② 分段函数
conditions = [data < 0, data == 0, data > 0]
choices = [-1, 0, 1]
result = np.select(conditions, choices)
# ③ 分组聚合(没有 groupby 但有替代方案)
labels = np.array(["a", "b", "a", "c", "b"])
values = np.array([10, 20, 30, 40, 50])
# 用 unique + 循环(分组数少时仍很快)
for label in np.unique(labels):
mask = labels == label
print(f"{label}: sum={values[mask].sum()}, mean={values[mask].mean()}")
4. 高级索引技巧
# ============================================================
# 布尔索引 —— 条件筛选
# ============================================================
data = np.random.randn(1000, 5)
# 筛选第3列 > 0 的所有行
filtered = data[data[:, 2] > 0]
# 多条件
mask = (data[:, 0] > 0) & (data[:, 1] < 0) # 用 & 不是 and
filtered = data[mask]
# ============================================================
# 花式索引 —— 任意顺序选择
# ============================================================
arr = np.array(["a", "b", "c", "d", "e"])
indices = [0, 2, 4, 1, 3]
print(arr[indices]) # ['a' 'c' 'e' 'b' 'd']
# 行列同时花式索引
matrix = np.arange(25).reshape(5, 5)
rows = [0, 2, 4]
cols = [1, 3]
# 需要 ix_ 取笛卡尔积
print(matrix[np.ix_(rows, cols)])
# ============================================================
# where 寻址
# ============================================================
arr = np.array([5, 2, 8, 1, 9, 3])
indices = np.where(arr > 4)
print(indices) # (array([0, 2, 4]),)
print(arr[indices]) # [5 8 9]
5. 性能优化技巧
5.1 避免不必要的复制
# ❌ 切片后再赋值会创建临时数组
a = np.random.randn(1000000)
b = a[::2].copy() # copy() 强制复制
# ✅ 直接使用视图
b = a[::2] # 视图,不复制数据
# ❌ 增量改变形状
for i in range(100):
a = a.reshape(100, 100) # 每次都复制
# ✅ 一次性 reshape
a = a.reshape(100, 100)
5.2 out 参数避免分配新内存
# ❌ 创建3个临时数组
result = np.sin(a) + np.cos(a) + np.tan(a)
# ✅ 使用 out 参数复用内存
result = np.empty_like(a)
np.sin(a, out=result)
np.cos(a, out=result) # 覆盖 result
# 这个例子不太合适——如需累加:
result = np.sin(a)
np.add(result, np.cos(a), out=result)
np.add(result, np.tan(a), out=result)
5.3 选择正确的数据类型
# float64 (8 bytes) → float32 (4 bytes) 内存减半
a = np.random.randn(10_000_000).astype(np.float32)
# float64 → int8 内存减至 1/8
labels = np.random.randint(0, 10, 10_000_000, dtype=np.int8)
6. NumPy vs Pandas:何时用哪个?
| 场景 | 推荐 |
|---|---|
| 纯数值矩阵运算 | NumPy |
| 图像/信号处理 | NumPy |
| 混合类型表格数据 | Pandas |
| 数据清洗/探索 | Pandas |
| 深度学习张量 | PyTorch/TensorFlow |
| 简单统计聚合 | 都可以 |
思考题
- 为什么
np.sin(arr)比[math.sin(x) for x in arr]快得多? - ndarray 的
view和copy有什么区别?什么时候它们是危险的? - 广播机制在什么情况下会失败?如何 debug 形状不匹配?
- 什么时候不应该用 NumPy(即纯 Python 反而更好)?