第 2.3 节 常见问题#
这一节收集了初学者在 NumPy 取数阶段最常卡的 10 个问题。每个问题都给出直接答案,能跑代码的都附上完整示例。
怎么用这一节 不用通读。遇到问题用 Ctrl+F 搜关键词,直接跳到对应那条。
准备一下数据,下面每题都会用到:
import numpy as np
scores = np.array([80, 90, 75, 100, 60])
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
])Q1:NumPy 数组的索引为什么从 0 开始?#
答: 因为 NumPy 沿用了 C 语言的规矩,而 Python 列表也一样。
原因很朴素:下标表示的是「往后数几个」。
- 第 1 个元素,往后数 0 个 → 下标
0; - 第 2 个元素,往后数 1 个 → 下标
1; - 第 5 个元素,往后数 4 个 → 下标
4。
这样算起来最省事,NumPy 底层做偏移计算也最快。
那
arr[1]到底是第 1 个还是第 2 个? 第 2 个。arr[1]是「下标为 1 的那个」,也就是第 2 个元素。 建议养成习惯,念的时候说「下标 1」而不是「第 1 个」,就不会绕晕。
print(scores[0]) # 80 ← 第 1 个元素
print(scores[1]) # 90 ← 第 2 个元素一个必须记住的差别:NumPy 的切片起点包含、终点不包含,和 Python 的 range 一致。
print(scores[1:4]) # [ 90 75 100] 下标 1、2、3Q2:如何访问二维数组中的指定行、列或单个元素?#
答: 逗号分隔:逗号前面管行,逗号后面管列。
print(matrix[0]) # [1 2 3] 第 0 行,整行
print(matrix[0, 1]) # 2 第 0 行,第 1 列
print(matrix[-1, -1]) # 9 最后一行,最后一列
print(matrix[:, 1]) # [2 5 8] 所有行,第 1 列
print(matrix[1, :]) # [4 5 6] 第 1 行,所有列记忆方法:: 的意思是「这一维全都要」。
| 写法 | 意思 |
|---|---|
matrix[0] | 第 0 行 |
matrix[0, 1] | 第 0 行、第 1 列的那个数 |
matrix[:, 1] | 所有行、第 1 列 |
matrix[1, :] | 第 1 行、所有列 |
matrix[:, :] | 整个数组(等价于 matrix) |
别把
matrix[0][1]和matrix[0, 1]搞混。 前者是「先取第 0 行(得到一个新数组),再取它的第 1 个」; 后者是「直接定位到第 0 行第 1 列」。 结果一样,但后者更快,而且对多维数组更清晰。
Q3:如何使用切片提取数组的一部分?#
答: 用 arr[起:止],起点包含、终点不包含。
print(scores[1:4]) # [ 90 75 100] 下标 1、2、3
print(scores[:3]) # [ 80 90 75] 开头到下标 2
print(scores[2:]) # [ 75 100 60] 下标 2 到最后
print(scores[:]) # 全部省略号 ... 不需要记,只要记住冒号的三种写法:
arr[:n]—— 前 n 个;arr[n:]—— 从第 n 个到最后;arr[:]—— 全部(复制一份)。
切片越界不会报错。
print(scores[2:100]) # [ 75 100 60] 只返回能取到的,不报错但单个索引越界会报错:
print(scores[5])IndexError: index 5 is out of bounds for axis 0 with size 5
Q4:NumPy 切片中的起始位置、结束位置和步长分别有什么作用?#
答: 完整写法是 arr[起始:结束:步长],三段都能省略。
| 位置 | 名字 | 作用 | 记法 |
|---|---|---|---|
| 第 1 段 | 起始位置 | 从哪儿开始 | 含 |
| 第 2 段 | 结束位置 | 到哪儿为止 | 不含 |
| 第 3 段 | 步长 | 隔几个取一个 | 省略就是 1 |
print(scores[::2]) # [80 75 60] 隔一个取一个
print(scores[1::2]) # [ 90 100] 从下标 1 开始,隔一个取一个
print(scores[::-1]) # [ 60 100 75 90 80] 倒过来负数步长就是倒着走,::-1 是最常用的「反转」写法。
二维数组切片也是两个维度各切各的:
print(matrix[0:2, 0:2]) # [[1 2]
# [4 5]]注意结果的形状会变小:matrix[0:2, 0:2] 的 shape 是 (2, 2),不是 (3, 3)。
Q5:什么是 Fancy Indexing?它与普通切片有什么区别?#
答: Fancy Indexing 就是「用一组下标,一次性取多个元素」——传进去的是列表或数组,不是一个整数或切片。
# 普通切片:取一段连续的
print(scores[1:4]) # [ 90 75 100]
# Fancy Indexing:取任意几个位置
print(scores[[0, 2, 4]]) # [80 75 60]两个关键区别:
切片 arr[1:4] | Fancy Indexing arr[[0, 2, 4]] | |
|---|---|---|
| 取的位置 | 必须连续 | 可以任意乱序、可以不连续 |
| 返回的是 | 视图(改了会动原数组) | 副本(改了原数组不变) |
| 能不能重复取 | 不能 | 能,arr[[0, 0, 1]] |
print(scores[[0, 0, 2]]) # [80 80 75] 同一个位置取两次记住这条就够用了: 切片给你「窗口」(视图),Fancy Indexing 给你「复印件」(副本)。 想清楚自己要的是窗口还是复印件,就不会出错了。
Q6:如何使用整数数组一次性提取多个不连续位置的元素?#
答: 把下标装进一个列表或数组,用 arr[[...]] 取。
# 用列表
print(scores[[0, 2, 4]]) # [80 75 60]
# 用数组也一样
idx = np.array([1, 3])
print(scores[idx]) # [ 90 100]还能用它「挑行挑列」:
print(matrix[[0, 2]]) # 取第 0 行和第 2 行[[1 2 3]
[7 8 9]]注意 arr[[0, 2]] 和 arr[0, 2] 完全不同!
print(matrix[0, 2]) # 3 ← 第 0 行第 2 列,一个数
print(matrix[[0, 2]]) # 两行 ← 取第 0 行和第 2 行一个判断方法:看方括号里有几个逗号、逗号两边是不是都用 [ ] 包起来。
[[0, 2]] 是 Fancy Indexing(列表套列表),[0, 2] 是二维索引。
Q7:什么是布尔掩码?如何使用多个条件进行筛选?#
答: 布尔掩码就是先用比较运算算出的一张 True/False 表,再用它当索引筛选。
mask = scores > 75
print(mask) # [ True True False True False]
print(scores[mask]) # [ 80 90 100]可以直接写在一行里,效果一样:
print(scores[scores > 75]) # [ 80 90 100]多个条件要用 &(并且)和 |(或者):
print(scores[(scores > 75) & (scores < 95)]) # [80 90]
print(scores[(scores > 90) | (scores < 70)]) # [100 60]
print(scores[~np.isin(scores, [75, 90])]) # [ 80 100 60]不能用 Python 的
and和or!high = scores > 75 low = scores < 85 scores[high and low] # ❌ 会报错 scores[high & low] # ✅
and/or是给单个布尔值用的(返回 True/False),而这里的high、low是数组, Python 会以为你要做逻辑运算,结果报这个错:ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()口诀:数组上的逻辑运算,一律用
&|~。
三个符号对照:
| 想要 | 写法 | 说明 |
|---|---|---|
| 并且 | a & b | 两个都满足 |
| 或者 | a | b | 满足一个就行 |
| 取反 | ~a | 把 True/False 互换 |
Q8:如何提取数组中满足条件的元素及其对应索引?#
答: 用 np.where(),它一次给你位置和元素。
pos = np.where(scores > 85)
print(pos)(array([1, 3]),)返回的是一个元组,里面装的是下标数组。二维数组上,返回的下标是二维的:
print(np.where(matrix > 4))(array([1, 1, 2, 2, 2]), array([1, 2, 0, 1, 2]))读法:第一个数组是「第几行」,第二个是「第几列」,一一对应。
rows, cols = np.where(matrix > 4)
print(rows) # [1 1 2 2 2] 行号
print(cols) # [1 2 0 1 2] 列号两两配对读:第 1 个满足条件的元素在「第 1 行第 1 列」,第 2 个在「第 1 行第 2 列」……
如果只想知道位置,不要元素,用 np.argwhere() 更直观:
print(np.argwhere(matrix > 4))[[1 1]
[1 2]
[2 0]
[2 1]
[2 2]]一句话记住区别
np.where(条件)→ 给你「下标」,用来定位;arr[条件]→ 给你「元素」,用来取数。np.argwhere(条件)→ 给你「坐标对」,最直观。
Q9:为什么修改切片得到的数组可能会影响原数组,而修改高级索引得到的数组通常不会?#
答: 因为切片返回的是「视图」,Fancy Indexing 和布尔掩码返回的是「副本」。
先看现象:
a = np.array([10, 20, 30, 40, 50])
part = a[1:3] # 切片
part[0] = 999
print(a)[ 10 999 30 40 50]**原数组被改了!**换成高级索引:
a = np.array([10, 20, 30, 40, 50])
part = a[[1, 2]] # Fancy Indexing
part[0] = 999
print(a)[10 20 30 40 50]原数组没变。
为什么? NumPy 把数组存在一块连续的内存里。
- 切片只是「从这块内存里截一段出来看」,不复制数据,所以叫视图(view)——改视图就是改那块内存;
- Fancy Indexing 和布尔掩码的位置不连续,没法用「一段连续内存」表示,NumPy 必须重新复制一份,所以叫副本(copy)。
怎么确认? 看 .base 属性:
a = np.array([10, 20, 30, 40, 50])
print(a[1:3].base is not None) # True ← 是视图
print(a[[1, 2]].base is not None) # False ← 是副本一句话总结 切片是「透过窗户看」,改窗户里的东西,屋里也跟着变; 高级索引是「拍照复印一份」,改复印件跟原件没关系。
Q10:如何避免数组赋值时意外修改原始数据?#
答: 三条路,按场景选。
办法一:想改原数组,就明确写出来。 用 arr[切片] = 值 直接赋值,别绕道中间变量。
a[1:3] = 999办法二:想要副本,加 .copy()。
r = a[1:3].copy() # 保险写法
r[0] = 999
print(a) # 原数组安全办法三:用 np.array() 也能拿副本。
r = np.array(a[1:3])最保险的一条:
a[1:3] = a[1:3] * 2 # 取出来、算、再放回去这种「取出来算完再写回去」的写法,无论中间是视图还是副本,结果都一样, 而且不用动脑子想它到底是不是视图。
小结#
这一节的 10 个问题,其实都围绕**「怎么从数组里精准取数」**:
- 下标从 0 开始——下标 = 往后数几个;
- 二维用逗号分隔,逗号前管行、后管列,
:表示全都要; - 切片
arr[起:止],起点含、终点不含; - 切片三件套:起始(含)、结束(不含)、步长(隔几个);
- Fancy Indexing 用下标列表取任意位置;
arr[[0,2]]是「取几行」,arr[0,2]是「第 0 行第 2 列」;- 布尔掩码 +
&|~做多条件筛选,别用andor; np.where()给位置,arr[条件]给元素;- 切片是视图、高级索引是副本——这是「改动会传染」的根源;
- 保险做法:
.copy(),或「取出来算完写回去」。
下一章预告 现在你能精准取数了。但还有个更厉害的东西等着你——形状完全不同的两个数组,居然能直接相乘相减,NumPy 会自动帮你「补齐」。这就是广播机制:
sales * city_factor[:, None]一行搞定「每个城市乘自己的系数」。第 3 章讲透。