NumPy: ndarray 核心
NumPy 的核心数据结构是 ndarray——N 维数组。它看起来像 Python 列表,但底层完全不同:固定类型、连续内存、向量化运算。理解 ndarray 的内部结构,是高效使用 NumPy 的第一步。
1. 你将学到
- ❶ ndarray 的本质:同质、固定类型、连续内存
- ❷ 维度(ndim)、形状(shape)、大小(size)
- ❸ 轴(axis)的概念直觉
- ❹ dtype 体系与内存占用
- ❺ ndarray 与 list 的内存模型对比
2. 一个数据分析师的真实故事
(1) 痛点:Excel 表格在 Python 里的困惑
Bob 拿到一张 3 行 4 列的 Excel 表格,用 Python 列表存储:每行一个子列表,3 个子列表嵌套在外层列表里。他想对第 2 列求和,只能写 for 循环逐行取值累加——100 万行数据要跑 5 秒。更糟的是,某行混入了一个字符串,循环跑到一半才报错。
(2) ndarray 解法
Charlie 建议 Bob 用 np.array 把表格转成 ndarray。NumPy 把数据铺成一块连续内存,用 C 语言级别的向量化操作一次性完成列求和——同样的 100 万行只需 5 毫秒。类型在创建时就统一了,混入字符串会立即报错而非运行到一半才崩溃。
(3) 收益
- 运算速度提升 1000 倍
- 类型不匹配在创建时就被捕获
- 形状、轴、步幅等元数据让多维操作变得直观
3. ndarray 的本质
(1) 同质数据类型
ndarray 的所有元素必须是同一类型。这一点和 Python 列表完全不同——列表可以混存整数、字符串甚至对象,而 ndarray 只允许一种 dtype。
import numpy as np
a = np.array([1, 2, 3])
print(a.dtype) # int64
b = np.array([1.0, 2.0, 3.0])
print(b.dtype) # float64
c = np.array([1, 2.0, 3])
print(c.dtype) # float64 (auto upcast)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) 连续内存存储
Python 列表存储的是指针数组——每个指针指向一个独立的 PyObject,分散在堆内存各处。ndarray 则把所有数据排列在一块连续内存中,没有指针开销,CPU 缓存命中率极高。
| 特性 | Python list | ndarray |
|---|---|---|
| 元素类型 | 任意混合 | 同一 dtype |
| 存储方式 | 指针数组 → 分散 PyObject | 连续内存块 |
| 每元素开销 | 28+ 字节(PyObject 头) | dtype 决定(1/2/4/8 字节) |
| 缓存友好性 | 差(指针跳转) | 极好(连续访问) |
| 类型安全 | 无(运行时才发现) | 创建时强制统一 |
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:0D/1D/2D/3D 数组属性(难度⭐)
import numpy as np
a0 = np.array(42) # 0D scalar
a1 = np.array([1, 2, 3]) # 1D
a2 = np.array([[1, 2], [3, 4]]) # 2D
a3 = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]]) # 3D
for name, arr in [("0D", a0), ("1D", a1), ("2D", a2), ("3D", a3)]:
print(f"{name}: ndim={arr.ndim}, shape={arr.shape}, size={arr.size}")
# 0D: ndim=0, shape=(), size=1
# 1D: ndim=1, shape=(3,), size=3
# 2D: ndim=2, shape=(2,2), size=4
# 3D: ndim=3, shape=(2,2,2), size=8
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
4. 维度、形状与大小
(1) ndim / shape / size
| 属性 | 含义 | 示例(shape=(3,4)) |
|---|---|---|
ndim |
维度数量(轴的个数) | 2 |
shape |
每个轴的长度,元组 | (3, 4) |
size |
总元素个数 = 各轴长度之积 | 12 |
import numpy as np
a = np.zeros((3, 4))
print(a.ndim) # 2
print(a.shape) # (3, 4)
print(a.size) # 12
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) strides:步幅
strides 告诉你:沿某个轴移动一步,在底层内存中需要跳过多少字节。
| 属性 | 含义 | shape=(3,4) dtype=int64 |
|---|---|---|
strides |
各轴步幅(字节) | (32, 8) |
itemsize |
单个元素字节数 | 8 |
import numpy as np
a = np.zeros((3, 4), dtype=np.int64)
print(a.strides) # (32, 8)
# axis 0: move 1 row = 4 * 8 = 32 bytes
# axis 1: move 1 col = 1 * 8 = 8 bytes
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:strides 可视化(难度⭐⭐)
import numpy as np
a = np.arange(12, dtype=np.int32).reshape(3, 4)
print(a)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
print(f"shape: {a.shape}") # (3, 4)
print(f"strides: {a.strides}") # (16, 4)
print(f"itemsize: {a.itemsize}") # 4
# axis 0 stride: 4 elements * 4 bytes = 16
# axis 1 stride: 1 element * 4 bytes = 4
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
5. 轴(axis)的概念直觉
轴是理解 ndarray 操作的关键。axis=0 是最外层维度,axis=1 是次外层,依此类推。
| 轴编号 | shape 中的位置 | 直觉 | 2D shape=(3,4) | 3D shape=(2,3,4) |
|---|---|---|---|---|
| axis=0 | 第 0 个 | 最外层 | 行方向(跨行) | 沿"层"方向 |
| axis=1 | 第 1 个 | 次外层 | 列方向(跨列) | 沿"行"方向 |
| axis=2 | 第 2 个 | 最内层 | — | 沿"列"方向 |
记忆口诀:axis=k 就是 shape[k] 对应的方向,对它做 sum/reduce 就是"消掉"那个维度。
import numpy as np
a = np.arange(24).reshape(2, 3, 4)
print(a.shape) # (2, 3, 4)
print(a.sum(axis=0).shape) # (3, 4) — axis 0 eliminated
print(a.sum(axis=1).shape) # (2, 4) — axis 1 eliminated
print(a.sum(axis=2).shape) # (2, 3) — axis 2 eliminated
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
6. dtype 与内存占用
(1) dtype 类型体系
| dtype | 说明 | 字节数 | 范围 |
|---|---|---|---|
np.int8 |
有符号 8 位整数 | 1 | -128 ~ 127 |
np.int32 |
有符号 32 位整数 | 4 | -2^31 ~ 2^31-1 |
np.int64 |
有符号 64 位整数 | 8 | -2^63 ~ 2^63-1 |
np.float32 |
32 位浮点 | 4 | ~±3.4e38,7 位精度 |
np.float64 |
64 位浮点 | 8 | ~±1.8e308,15 位精度 |
np.bool_ |
布尔 | 1 | True/False |
np.complex128 |
128 位复数 | 16 | 两个 float64 |
(2) 内存模型:ndarray 不是嵌套列表
graph TB
A["ndarray object"] --> B["data pointer<br/>raw memory block"]
A --> C["dtype<br/>element type"]
A --> D["shape<br/>(3, 4)"]
A --> E["strides<br/>(32, 8)"]
B --> F["0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | ... | 2,3"]
style A fill:#4CAF50,color:#fff
style B fill:#2196F3,color:#fff
style F fill:#FF9800,color:#fff
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(3) 形状与内存对照表
| shape | ndim | size | dtype=float64 nbytes | strides |
|---|---|---|---|---|
| (10,) | 1 | 10 | 80 | (8,) |
| (3, 4) | 2 | 12 | 96 | (32, 8) |
| (2, 3, 4) | 3 | 24 | 192 | (96, 32, 8) |
| (5, 2, 3, 4) | 4 | 120 | 960 | (192, 96, 32, 8) |
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:dtype 内存占用对比(难度⭐)
import numpy as np
for dtype in [np.int8, np.int32, np.int64, np.float32, np.float64]:
a = np.zeros(1000, dtype=dtype)
print(f"{dtype.__name__:10s} itemsize={a.itemsize} nbytes={a.nbytes}")
# int8 itemsize=1 nbytes=1000
# int32 itemsize=4 nbytes=4000
# int64 itemsize=8 nbytes=8000
# float32 itemsize=4 nbytes=4000
# float64 itemsize=8 nbytes=8000
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:reshape 不复制数据验证(难度⭐⭐)
import numpy as np
a = np.arange(12, dtype=np.int64)
b = a.reshape(3, 4)
print(a.shape, b.shape) # (12,) (3, 4)
print(a.strides, b.strides) # (8,) (32, 8)
print(a.nbytes, b.nbytes) # 96 96
b[0, 0] = 999
print(a[0]) # 999 — same memory!
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
7. 综合示例:3D 数组全解析
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:3×4×2 数组全属性分析(难度⭐⭐⭐)
import numpy as np
a = np.arange(24, dtype=np.int64).reshape(3, 4, 2)
print("=== Full array ===")
print(a)
# [[[ 0 1]
# [ 2 3]
# [ 4 5]
# [ 6 7]]
# [[ 8 9]
# [10 11]
# [12 13]
# [14 15]]
# [[16 17]
# [18 19]
# [20 21]
# [22 23]]]
print(f"ndim: {a.ndim}") # 3
print(f"shape: {a.shape}") # (3, 4, 2)
print(f"size: {a.size}") # 24
print(f"dtype: {a.dtype}") # int64
print(f"itemsize:{a.itemsize}") # 8
print(f"nbytes: {a.nbytes}") # 192 (24 * 8)
print(f"strides: {a.strides}") # (64, 16, 8)
# axis 0: 4*2*8 = 64 bytes per step
# axis 1: 2*8 = 16 bytes per step
# axis 2: 1*8 = 8 bytes per step
# Verify strides manually
expected_strides = (
a.shape[1] * a.shape[2] * a.itemsize, # 4*2*8 = 64
a.shape[2] * a.itemsize, # 2*8 = 16
a.itemsize # 8
)
print(f"manual strides: {expected_strides}") # (64, 16, 8)
print(f"match: {a.strides == expected_strides}") # True
# axis reduction
print(a.sum(axis=0).shape) # (4, 2)
print(a.sum(axis=1).shape) # (3, 2)
print(a.sum(axis=2).shape) # (3, 4)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
❓ 常见问题
📖 小节
- ndarray 是同质、固定类型、连续内存存储的 N 维数组
ndim是维度数,shape是各轴长度元组,size是总元素数- 轴(axis)从外到内编号:axis=0 最外层,axis=ndim-1 最内层
strides描述沿各轴移动的内存跳越量,reshape 不改变底层数据dtype决定元素类型和itemsize,nbytes = size * itemsize- ndarray 的连续内存模型 vs 列表的指针跳转模型,是性能差距的根源
📝 作业
-
基础题(难度⭐):创建 4 个 dtype 分别为 int8、int32、float32、float64 的长度为 5000 的数组,打印各自的
nbytes,解释为什么不同 dtype 占用内存不同。 -
进阶题(难度⭐⭐):给定一个 shape=(5, 3, 2)、dtype=int16 的 ndarray,手算其 strides 和 nbytes,然后用代码验证你的计算。
-
挑战题(难度⭐⭐⭐):用你自己的语言解释 shape=(2,3,4) 的 ndarray 在内存中如何排布,画出示意图,标出 strides 和每个元素在内存中的偏移量。