NumPy: ndarray 核心

NumPy 的核心数据结构是 ndarray——N 维数组。它看起来像 Python 列表,但底层完全不同:固定类型、连续内存、向量化运算。理解 ndarray 的内部结构,是高效使用 NumPy 的第一步。

1. 你将学到


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) 收益


3. ndarray 的本质

(1) 同质数据类型

ndarray 的所有元素必须是同一类型。这一点和 Python 列表完全不同——列表可以混存整数、字符串甚至对象,而 ndarray 只允许一种 dtype。

PYTHON
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)
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。

(2) 连续内存存储

Python 列表存储的是指针数组——每个指针指向一个独立的 PyObject,分散在堆内存各处。ndarray 则把所有数据排列在一块连续内存中,没有指针开销,CPU 缓存命中率极高。

特性 Python list ndarray
元素类型 任意混合 同一 dtype
存储方式 指针数组 → 分散 PyObject 连续内存块
每元素开销 28+ 字节(PyObject 头) dtype 决定(1/2/4/8 字节)
缓存友好性 差(指针跳转) 极好(连续访问)
类型安全 无(运行时才发现) 创建时强制统一

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:0D/1D/2D/3D 数组属性(难度⭐)

PYTHON
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
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。


4. 维度、形状与大小

(1) ndim / shape / size

属性 含义 示例(shape=(3,4))
ndim 维度数量(轴的个数) 2
shape 每个轴的长度,元组 (3, 4)
size 总元素个数 = 各轴长度之积 12
PYTHON
import numpy as np

a = np.zeros((3, 4))
print(a.ndim)    # 2
print(a.shape)   # (3, 4)
print(a.size)    # 12
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。

(2) strides:步幅

strides 告诉你:沿某个轴移动一步,在底层内存中需要跳过多少字节。

属性 含义 shape=(3,4) dtype=int64
strides 各轴步幅(字节) (32, 8)
itemsize 单个元素字节数 8
PYTHON
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
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:strides 可视化(难度⭐⭐)

PYTHON
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
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。


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 就是"消掉"那个维度。

PYTHON
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
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。


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 不是嵌套列表

100%
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
TEXT 📖 仅展示
> **输出:** 在本地 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)

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:dtype 内存占用对比(难度⭐)

PYTHON
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
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:reshape 不复制数据验证(难度⭐⭐)

PYTHON
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!
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。


7. 综合示例:3D 数组全解析

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:3×4×2 数组全属性分析(难度⭐⭐⭐)

PYTHON
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)
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。


❓ 常见问题

Q ndarray 可以存不同类型吗?
A 不可以。ndarray 是同质的,所有元素必须是同一个 dtype。如果尝试混合类型,NumPy 会自动向上转型(如 int→float),无法兼容的会报错。
Q axis=0 和 axis=1 哪个是行?
A 在 2D 数组中,axis=0 是行方向(跨行),axis=1 是列方向(跨列)。沿 axis=0 做 sum 就是"把各行压成一行",沿 axis=1 做 sum 就是"把各列压成一列"。
Q strides 是什么?
A strides 是一个元组,表示沿每个轴移动一步需要在内存中跳过多少字节。它是 ndarray 不复制数据就能实现 reshape/transpose 的关键。
Q float64 和 float32 区别?
A float64 占 8 字节,约 15 位十进制精度;float32 占 4 字节,约 7 位精度。float32 节省一半内存,但精度较低,深度学习常用 float32,科学计算常用 float64。
Q 为什么说 ndarray 是"同质"的?
A 因为 ndarray 的所有元素在内存中紧密排列,每个元素占相同字节数、解释方式相同(由 dtype 统一规定)。这使 CPU 可以用向量化指令一次处理多个元素,是 NumPy 高性能的根本原因。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建 4 个 dtype 分别为 int8、int32、float32、float64 的长度为 5000 的数组,打印各自的 nbytes,解释为什么不同 dtype 占用内存不同。

  2. 进阶题(难度⭐⭐):给定一个 shape=(5, 3, 2)、dtype=int16 的 ndarray,手算其 strides 和 nbytes,然后用代码验证你的计算。

  3. 挑战题(难度⭐⭐⭐):用你自己的语言解释 shape=(2,3,4) 的 ndarray 在内存中如何排布,画出示意图,标出 strides 和每个元素在内存中的偏移量。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏