RSS
菜单
全部文章快讯开发科技深度热点

第 46 章 NumPy 数组基础与创建

内容摘要

本篇正式进入 pandas 的"地基"——NumPy ndarray。我们会对比 ndarray 与 Python list 的本质区别,讲清 dtype/shape/ndim/size/itemsize 五大属性,系统演示 np.array、arange、linspace、zeros、ones、eye、full、empty 八种创建方式与 random 随机数套件,再覆盖 reshape/ravel/flatten/转置、正负索引、步长切片、布尔索引、花式索引以及"视图 vs 副本"这一最容易踩坑的主题。前置知识是本系列前 45 章的 pandas 基础操作。学完后你会真正看懂"pandas 底层到底是什么",并为第 47 章广播与向量化、第 48 章 NumPy 与 pandas 协同打好基础。

📖 本篇简介:本篇正式进入 pandas 的"地基"——NumPy ndarray。我们会对比 ndarray 与 Python list 的本质区别,讲清 dtype/shape/ndim/size/itemsize 五大属性,系统演示 np.array、arange、linspace、zeros、ones、eye、full、empty 八种创建方式与 random 随机数套件,再覆盖 reshape/ravel/flatten/转置、正负索引、步长切片、布尔索引、花式索引以及"视图 vs 副本"这一最容易踩坑的主题。前置知识是本系列前 45 章的 pandas 基础操作。学完后你会真正看懂"pandas 底层到底是什么",并为第 47 章广播与向量化、第 48 章 NumPy 与 pandas 协同打好基础。
你在前面章节写的 df["col"] * 2、df.groupby().sum(),背后跑的都是 NumPy。本篇把这层"地下结构"挖出来看清楚。

目录

1. 为什么学 NumPy

2. ndarray 与 Python list 的本质区别

3. ndarray 的核心属性

4. 用 array 从列表创建数组

5. 等差序列 arange 与 linspace

6. 特殊数组 zeros ones eye full empty

7. 随机数生成

8. 形状操作 reshape ravel flatten 与转置

9. 索引与切片

10. 布尔索引与花式索引

11. 视图与副本

12. NumPy 与 pandas 的关系

13. 常见坑

14. 本章小结与练习


1. 为什么学 NumPy

NumPy(Numerical Python)是 Python 生态里几乎所有科学计算库的底层依赖:pandas 的 Series/DataFrame、scikit-learn 的训练数据、matplotlib 的绘图数据,底层都是 NumPy 的 ndarray(n-dimensional array,多维数组)。

学 NumPy 的理由很实际:

  • 看懂 pandas 的报错:很多 pandas 异常最终是 NumPy 的 ValueError(形状不匹配、类型不兼容),看不懂 NumPy 就只能瞎改。
  • 写出更快的代码:pandas 的性能优化本质就是"把循环下沉到 NumPy 的 C 层",这正是第 47 章的主题。
  • 无缝交接给其他库:df.values 拿到的就是 ndarray,直接喂给 sklearn / scipy / matplotlib。

环境确认(本机 Windows / Python 3.14.7 / numpy 2.5.3 / pandas 3.0.6):

import numpy as np
import pandas as pd

print("numpy", np.__version__)
print("pandas", pd.__version__)
numpy 2.5.3
pandas 3.0.6

惯例是 import numpy as np,几乎所有中文教程和开源项目都用这个别名。


2. ndarray 与 Python list 的本质区别

很多人说"ndarray 就是快速的 list",这个说法不准确。它们的区别是数据结构层面的:

| 维度 | Python list | ndarray |

|------|-------------|---------|

| 元素类型 | 可以混合(int、str、对象混放) | 同一块连续内存,所有元素 dtype 相同 |

| 内存布局 | 分散的对象指针链表 | 连续的 C 风格数组 |

| 运算语义 | list * 2 是"重复拼接" | arr * 2 是"逐元素计算" |

| 向量化 | 不支持,只能 for 循环 | 原生支持,直接下沉 C 层 |

| 多维 | 需要嵌套 list,操作笨拙 | 原生多维,shape 管理 |

直接看最经典的对比:

py_list = [1, 2, 3, 4, 5]
arr = np.array([1, 2, 3, 4, 5])

print("list * 2 =", py_list * 2)   # list 是把自己重复一遍
print("array * 2 =", arr * 2)       # ndarray 是每个元素乘 2
print("list + list =", py_list + py_list)
print("array + array =", arr + arr)
list * 2 = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
array * 2 = [ 2  4  6  8 10]
list + list = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
array + array = [ 2  4  6  8 10]

这就是"向量化"的直观感受:对整个数组说 arr * 2,NumPy 在 C 层一次性遍历 100 万元素,而不用你写一个 Python for 循环。

内存占用差距同样惊人(本机实测):

import sys
lst = list(range(1_000_000))   # Python list,存的是"指向 int 对象的指针"
arr1m = np.arange(1_000_000)   # ndarray,连续内存块

print("Python list 1e6 元素占用(字节):", sys.getsizeof(lst))
print("ndarray nbytes:", arr1m.nbytes)
Python list 1e6 元素占用(字节): 8000056
ndarray nbytes: 8000000

注意:这里 list 的 8MB 只是指针数组的大小,100 万个 int 对象本身还各自占 28 字节左右,实际开销远大于此;而 ndarray 的 8MB 就是全部(int64 每个元素 8 字节)。当你在 pandas 里读入百万行数值列,省下的就是这种结构性开销。

⚠️ 常见坑:不要把 ndarray 当 list 用。arr.append(...) 是不存在的(ndarray 长度固定),arr + [1,2] 也不是拼接。需要"动态追加"请用 list 攒好再一次性 np.array()。

3. ndarray 的核心属性

拿到一个 ndarray,先问五个问题:它是什么类型?几行几列?几维?多少个元素?每个元素占几个字节?这五个问题对应五个属性:

a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32)

print("dtype:", a.dtype)      # 元素数据类型
print("shape:", a.shape)       # 各维度长度,元组
print("ndim:", a.ndim)         # 维度数 = len(shape)
print("size:", a.size)          # 元素总数 = shape 各维乘积
print("itemsize:", a.itemsize) # 单个元素占多少字节
print("nbytes:", a.nbytes)      # 总字节数 = size * itemsize
dtype: int32
shape: (2, 3)
ndim: 2
size: 6
itemsize: 4 bytes
nbytes: 24

速查表:

| 属性 | 含义 | 本例值 |

|------|------|--------|

| dtype | 元素类型(int32/float64/bool/…) | int32 |

| shape | 各维长度组成的元组 | (2, 3) |

| ndim | 维度数(轴数) | 2 |

| size | 元素总数 | 6 |

| itemsize | 单元素字节数 | 4 |

| nbytes | 数据区总字节数 | 24 |

几个常用 dtype:np.int32、np.int64(64 位机器默认整数)、np.float32、np.float64(默认浮点)、np.bool_、np.str_。pandas 3.0 里字符串列默认后端已是 str dtype,底层与这里的 np.str_ 同源。


4. 用 array 从列表创建数组

最直接的创建方式是 np.array(),传入一个 Python list(一维)或嵌套 list(多维):

# 一维:传一层列表
v = np.array([1, 2, 3, 4])
print(v, v.shape)

# 二维:传嵌套列表,内层每个子列表是一行
m = np.array([[1, 2, 3], [4, 5, 6]])
print(m)

# 用 dtype 指定类型
f = np.array([1, 2, 3], dtype=np.float64)
print(f, f.dtype)
[1 2 3 4] (4,)
[[1 2 3]
 [4 5 6]]
[1. 2. 3.] float64
⚠️ 常见坑:类型自动"向上转型"。ndarray 要求所有元素同 dtype,所以当你混入不同类型时,NumPy 会自动统一,结果往往不是你想要的:
d = np.array([1, "two", 3.0])   # int + str + float
print(d, d.dtype)
['1' 'two' '3.0'] <U32

三个数字字符串被统一成了字符串 dtype(<U32 表示最多 32 字符的 Unicode),原本的 1 和 3.0 都变成了文本,再也没法直接做加法。建数组前就要保证列表里元素类型一致,这也是 pandas 里读数据后要立刻 astype 的原因。


5. 等差序列 arange 与 linspace

造测试数据时,最常用的两个函数:

# arange(start, stop, step):类似 Python 的 range,左闭右开
print("arange(0, 10, 2):", np.arange(0, 10, 2))
print("arange(5):", np.arange(5))
print("arange(0, 1, 0.2):", np.arange(0, 1, 0.2))

# linspace(start, stop, num):等距切 num 个点,默认包含右端点
print("linspace(0, 1, 5):", np.linspace(0, 1, 5))
print("endpoint=False:", np.linspace(0, 1, 5, endpoint=False))
arange(0, 10, 2): [0 2 4 6 8]
arange(5): [0 1 2 3 4]
arange(0, 1, 0.2): [0.  0.2 0.4 0.6 0.8]
linspace(0, 1, 5): [0.   0.25 0.5  0.75 1.  ]
endpoint=False: [0.  0.2 0.4 0.6 0.8]

选择建议:知道步长用 arange(点数由步长决定);知道点数用 linspace(步长由点数决定),画函数曲线、等距采样时更好用。

⚠️ 常见坑:arange 用浮点步长时,由于浮点累加误差,点数可能和你预期差 1。例如 np.arange(0, 1.1, 0.1) 的右端点行为不稳定。需要精确点数时优先 linspace。

6. 特殊数组 zeros ones eye full empty

造矩阵骨架时,下面几个函数比手写嵌套 list 快得多:

print("zeros((2,3)):\n", np.zeros((2, 3)))     # 全 0,默认 float64
print("ones(3):", np.ones(3))                   # 全 1
print("eye(3):\n", np.eye(3))                  # 单位矩阵(对角线为 1)
print("full((2,2), 7):\n", np.full((2, 2), 7)) # 全部填同一个值
zeros((2,3)):
 [[0. 0. 0.]
 [0. 0. 0.]]
ones(3): [1. 1. 1.]
eye(3):
 [[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
full((2,2), 7):
 [[7 7]
 [7 7]]

最后一个 np.empty 最容易被误解:

e = np.empty(3)
print("empty(3) 未初始化:", e)
empty(3) 未初始化: [1. 1. 1.]

empty 只是分配内存、不写初始值,里面是那块内存上残留的"垃圾数据"(本例刚好是上一行 ones(3) 留下的 1)。它比 zeros 快,因为省了一次写 0 的动作,但绝对不要在没有立刻填满之前就去读它。正常业务用 zeros/ones/full,只有在你接下来马上要用计算结果覆盖整段内存时,才为了那点性能去用 empty。

| 函数 | 作用 | 默认 dtype |

|------|------|-----------|

| np.zeros(shape) | 全 0 数组 | float64 |

| np.ones(shape) | 全 1 数组 | float64 |

| np.eye(n) | n×n 单位矩阵 | float64 |

| np.full(shape, v) | 全部填 v | 按 v 推断 |

| np.empty(shape) | 分配但不初始化 | 按要求 |

| np.ones_like(a) | 和 a 同 shape 的全 1 | 与 a 相同 |


7. 随机数生成

做演示、造测试数据、切片抽样都离不开随机数。NumPy 的随机数在子模块 np.random 里。第一件永远要做的事是设种子:

np.random.seed(42)                 # 固定种子,保证每次运行结果可复现
print("rand(2,3):\n", np.random.rand(2, 3))   # 均匀分布 [0,1)
np.random.seed(42)
print("randn(3):", np.random.randn(3))        # 标准正态分布
np.random.seed(42)
print("randint(1,10,size=5):", np.random.randint(1, 10, size=5))  # 随机整数
np.random.seed(42)
print("uniform(0,1,3):", np.random.uniform(0, 1, 3))                # 均匀分布
np.random.seed(42)
print("choice:", np.random.choice(["a", "b", "c"], 5))              # 随机抽样
rand(2,3):
 [[0.37454012 0.95071431 0.73199394]
 [0.59865848 0.15601864 0.15599452]]
randn(3): [ 0.49671415 -0.1382643   0.64768854]
randint(1,10,size=5): [7 4 8 5 7]
uniform(0,1,3): [0.37454012 0.95071431 0.73199394]
choice: ['c' 'a' 'c' 'c' 'a']

要点:

  • seed(42) 让随机序列可复现。教程、单测、汇报代码每次跑出来的数都一样,别人才复现得了你的结论。
  • rand 均匀分布、randn 标准正态(均值 0 方差 1),名字相近别混;choice 从给定序列里有放回抽样,pandas 里 df.sample() 底层思路类似。

8. 形状操作 reshape ravel flatten 与转置

数据经常要"变形"而不改数值:

x = np.arange(12)
print("arange(12):", x)

# reshape:把 12 个元素重排成 3 行 4 列
print("reshape(3,4):\n", x.reshape(3, 4))

# -1 表示"这一维我懒得算,你根据总数自己推"
print("reshape(-1,4):\n", x.reshape(-1, 4))

m = x.reshape(3, 4)
print("ravel:", m.ravel())      # 拉平成一维,尽量返回视图
print("flatten:", m.flatten())  # 拉平成一维,永远返回副本
print("T 转置:\n", m.T)         # 行列互换
arange(12): [ 0  1  2  3  4  5  6  7  8  9 10 11]
reshape(3,4):
 [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
reshape(-1,4):
 [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
ravel: [ 0  1  2  3  4  5  6  7  8  9 10 11]
flatten: [ 0  1  2  3  4  5  6  7  8  9 10 11]
T 转置:
 [[ 0  4  8]
 [ 1  5  9]
 [ 2  6 10]
 [ 3  7 11]]

reshape 的要求是新 shape 的元素总数和原数组完全相等(3×4 = 12)。-1 是语法糖:你给一个维度,NumPy 反推另一个。

ravel vs flatten 的区别下一节"视图与副本"会展开:想省内存用 ravel(),想要一份独立数据用 flatten()。


9. 索引与切片

NumPy 索引基本沿用 Python list 的语法,但支持多维:

v = np.arange(10, 20)          # [10..19]
print("v:", v)
print("v[0]:", v[0], "v[-1]:", v[-1])   # 正负索引都认
print("v[2:7]:", v[2:7])                 # 切片,左闭右开
print("v[::2]:", v[::2])                 # 步长 2
print("v[::-1]:", v[::-1])               # 负步长 = 反转
v: [10 11 12 13 14 15 16 17 18 19]
v[0]: 10 v[-1]: 19
v[2:7]: [12 13 14 15 16]
v[::2]: [10 12 14 16 18]
v[::-1]: [19 18 17 16 15 14 13 12 11 10]

二维数组用"逗号分隔各维度"的方式索引,比嵌套 list 的 mat[row][col] 清晰:

mat = np.arange(1, 13).reshape(3, 4)
print("mat:\n", mat)
print("mat[1, 2]:", mat[1, 2])     # 第 2 行第 3 列(从 0 数)
print("mat[0]:", mat[0])            # 取第 0 行
print("mat[:, 1]:", mat[:, 1])      # : 表示整维,这里取第 1 列
print("mat[0:2, 1:3]:\n", mat[0:2, 1:3])  # 行列同时切片
mat:
 [[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]
mat[1, 2]: 7
mat[0]: [1 2 3 4]
mat[:, 1]: [ 2  6 10]
mat[0:2, 1:3]:
 [[2 3]
 [6 7]]

记住这个心智模型:每个逗号隔开一个轴。三维数组就是 arr[i, j, k]。


10. 布尔索引与花式索引

这两种索引是 NumPy(也是 pandas)筛选数据的灵魂。

布尔索引:传一个和数组等长的布尔数组,挑出 True 位置的元素:

v = np.arange(10, 20)
print("v[v > 15]:", v[v > 15])        # 先算 v>15 得到布尔数组,再拿来筛
print("np.where(v > 15):", np.where(v > 15))   # where 返回满足条件的下标
v[v > 15]: [16 17 18 19]
np.where(v > 15): (array([6, 7, 8, 9]),)

花式索引(fancy indexing):传一个整数数组(或列表),按顺序挑出这些位置:

print("v[[1, 3, 5]]:", v[[1, 3, 5]])   # 挑下标 1、3、5
print("mat[[0,2]]:\n", mat[[0, 2]])   # 二维里挑第 0、2 行
print("mat[mat > 6]:", mat[mat > 6])  # 布尔筛选后自动拉平
v[[1, 3, 5]]: [11 13 15]
mat[[0,2]]:
 [[ 1  2  3  4]
 [ 9 10 11 12]]
mat[mat > 6]: [ 7  8  9 10 11 12]

对比记忆:

| 方式 | 传入 | 用途 |

|------|------|------|

| 切片 | start:stop:step | 取连续一段 |

| 布尔索引 | 布尔数组 | 按条件筛选 |

| 花式索引 | 整数数组/列表 | 按指定下标挑选、重排 |

⚠️ 常见坑:多个条件要用括号 + &/|/~,不能用 Python 的 and/or/not:
v[(v > 12) & (v < 18)]     # ✅ 对
v[v > 12 and v < 18]       # ❌ ValueError,and 不能作用于数组
这个坑在第 47 章布尔掩码还会详细展开。

11. 视图与副本

这是 NumPy(以及第 18 章讲过的 pandas SettingWithCopyWarning)最经典的坑。切片得到的是视图,不是副本——它和原数组共享同一块内存:

src = np.array([1, 2, 3, 4, 5])
view = src[1:4]     # 切片,是视图
view[0] = 999       # 改视图……
print("改 view 后 src:", src)   # 原数组跟着变了!
改 view 后 src: [  1 999   3   4   5]

view[0] 本是 src[1],改它等于改 src[1]。你以为在操作一个独立切片,实际动了原始数据。想要一份互不影响的拷贝,显式 .copy():

src2 = np.array([1, 2, 3, 4, 5])
cp = src2[1:4].copy()
cp[0] = 999
print(".copy() 后 src2:", src2)
.copy() 后 src2: [1 2 3 4 5]

什么时候需要 .copy():

  • 你要在切片上做修改,但不想影响原数组;或把切片传给别人的函数,不希望对方内部修改波及你的原始数据;
  • 后面接 ravel()/转置这类返回视图的操作时,如果要长期保存结果,先 copy。

什么时候不用 copy:只读不写、只是拿切片做计算/画图时,视图反而省内存(不复制数据)。

对应到 pandas:第 18 章讲过的 SettingWithCopyWarning,根源就是"链式索引可能拿到视图也可能拿到副本"。pandas 3.0 起默认开启 Copy-on-Write(CoW),链式赋值不再触发该警告,行为变得可预期;但 NumPy 原生 ndarray 没有 CoW,切片即视图的规则始终成立。

12. NumPy 与 pandas 的关系

最后把本篇和系列主线接上。pandas 的 Series 和 DataFrame,数值列的底层就是一个 ndarray:

import pandas as pd

s = pd.Series([1, 2, 3, 4])
print("Series.values:", s.values, type(s.values))

df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
print("df['a'].values:", df["a"].values, type(df["a"].values))
Series.values: [1 2 3 4] <class 'numpy.ndarray'>
df['a'].values: [1 2] <class 'numpy.ndarray'>

也就是说:

  • 你对 df["a"] 做的 * 2、.sum()、布尔筛选,绝大多数最终都翻译成了本篇讲的 ndarray 操作;
  • df.values / df.to_numpy() 直接把底层 ndarray 交出来,可无缝喂给 sklearn、matplotlib、scipy;反过来任何 ndarray 都能直接 pd.DataFrame(arr) 包成表。

理解了 ndarray 的 dtype、shape、视图语义,pandas 里"为什么这列是 object""为什么改了切片原数据也变了""为什么两列相加报形状错误"这类问题都会迎刃而解。


13. 常见坑

1. 把 list 语义套到 ndarray 上:arr.append()、arr + [1] 都不存在;arr * 2 是逐元素而不是重复。

2. 混合类型自动转型:np.array([1, "x", 3.0]) 会整体变成字符串,数值列建数组前先统一类型。

3. `np.empty` 里是垃圾值:分配后没填满就读,会拿到不可预测的数据。

4. `arange` 浮点步长点数漂移:需要精确点数用 linspace。

5. 切片是视图:改切片波及原数组,需要独立数据就 .copy()。

6. 多条件用 `and/or/not`:数组多条件必须写 (a > 1) & (b < 2),且每个条件加括号。

7. 不设随机种子:教程/实验代码每次跑结果不同,无法复现。


14. 本章小结与练习

小结:ndarray 是同类型、连续内存、可向量化的多维数组;五大属性 dtype/shape/ndim/size/itemsize 描述它;创建看 array/arange/linspace/zeros/ones/eye/full/empty;变形靠 reshape/ravel/flatten/T;筛选靠切片、布尔索引与花式索引;切片是视图,独立数据要 .copy();pandas 列的底层就是 ndarray。

练习:

1. 用三种不同方式创建一个长度为 10、全为 3.14 的一维数组(提示:np.full、np.ones 再乘、np.array),并说出各自 dtype。

2. 创建 np.arange(20).reshape(4, 5),取出它的第 2 列、取出右下角 2×2 子矩阵、再把它转置。

3. 给定 arr = np.array([3, -1, 4, 1, 5, -9, 2, 6]),用布尔索引挑出所有绝对值大于等于 3 的元素,并验证你写出的条件表达式。

4. 解释下面这段代码最后 a 的值是什么,并说出原因:

a = np.array([10, 20, 30, 40])
b = a[1:3]
b[0] = 999

5. np.empty(5) 每次运行结果都一样吗?为什么?在什么场景下你会选择它而不是 np.zeros(5)?

6. 把一个 10×10 的 ndarray 通过 .values 的视角反向验证:先用 pd.DataFrame 包一个 5 行 3 列的随机数组,再用 df.to_numpy() 取出底层数组,确认它的 shape 与 dtype。


下一篇预告:第 47 章 NumPy 广播与向量化计算 —— 形状不同的数组怎么做加法?向量化凭什么比 Python 循环快几十上百倍?本篇用本机实测数据给出答案。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 46 章 NumPy 数组基础与创建》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。