第 2.1 节 核心知识点#
预计学习时长: 40 分钟
本节目标:
- 会用正数、负数索引取单个元素;
- 会用切片取一段,并且知道起点含、终点不含;
- 会用
arr[[...]]一次性取多个不连续的位置;- 会用布尔掩码筛选,包括多个条件同时满足;
- 知道切片和高级索引的区别,不会再不小心改到原数组。
先说清楚一件事:四种取数方式,两种结果类型#
准备数据:
import numpy as np
scores = np.array([80, 90, 75, 100, 60])
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
])先把四种取数方式摆在一起:
| 写法 | 返回 | 类型 | 改它会改原数组吗 |
|---|---|---|---|
scores[0] | 一个数 | np.int64 | 不会 |
scores[1:3] | 一段 | ndarray | 会(视图) |
scores[[0, 2]] | 一段 | ndarray | 不会(副本) |
scores[scores > 75] | 一段 | ndarray | 不会(副本) |
「会」和「不会」这一列,就是本章要讲透的核心。
知识点 1:基础索引(Basic Indexing)#
取单个元素:方括号里写数字#
print(scores[0]) # 80
print(scores[2]) # 75从 0 开始数——和 Python 列表一模一样。
负数索引:从后往前数#
print(scores[-1]) # 60 最后一个
print(scores[-2]) # 100 倒数第二个越界会直接报错#
scores[5]
# IndexError: index 5 is out of bounds for axis 0 with size 5注意 NumPy 和 Pandas 的一个区别 在 Pandas 里
df[0]是取「叫 0 的那一列」;在 NumPy 里arr[0]是取「第 0 行的第一个元素」。 语义完全不同,别混。
二维数组:两个下标#
二维数组用 arr[行, 列],行列之间是逗号:
print(matrix[0]) # 第 0 行,整行[1 2 3]print(matrix[0, 1]) # 第 0 行,第 1 列2print(matrix[-1, -1]) # 最后一行最后一列9「行、列」还是「列、行」? NumPy 是
[行, 列],和数学里的矩阵记号一致。 这跟 Pandas 的df.loc[行, 列]一样,但跟df.iloc[行][列]那种链式写法不一样——NumPy 不需要链式,逗号直接写。
取一整行 / 一整列#
print(matrix[:, 1]) # 所有行,第 1 列[2 5 8]print(matrix[1, :]) # 第 1 行,所有列[4 5 6]这里的冒号 : 是「这一维全都要」。
| 写法 | 含义 |
|---|---|
matrix[0] | 第 0 行,所有列 |
matrix[:, 1] | 所有行,第 1 列 |
matrix[1, :] | 第 1 行,所有列 |
matrix[:, :] | 整个数组(等价于 matrix) |
知识点 2:切片(Fancy Indexing 之前先搞懂切片)#
切片的三个要素#
scores[开始:结束:步长]Python 规则:起点包含,终点不包含。
print(scores[1:4]) # 90, 75, 100 (取位置 1、2、3)
print(scores[:3]) # 80, 90, 75
print(scores[2:]) # 75, 100, 60
print(scores[:]) # 全部步长#
print(scores[::2]) # 80, 75, 60 隔一个取一个
print(scores[1::2]) # 90, 100
print(scores[::-1]) # 倒过来[80 75 60]
[ 90 100]
[ 60 100 75 90 80]切片不会因为越界而报错#
print(scores[2:100]) # 只返回能取到的那些,不报错[ 75 100 60]对比一下 Pandas
- NumPy:
arr[0:2]→ 取位置 0、1(不含 2)- Pandas:
df.loc[0:2]→ 取标签 0、1、2(含 2)这两个切片规则不一样,是很多人从 Pandas 转到 NumPy 时踩的第一个坑。
二维数组也能切片#
print(matrix[0:2, 0:2])[[1 2]
[4 5]]两个维度各自切。 matrix[行范围, 列范围],中间还是逗号。
知识点 3:Fancy Indexing(高级索引)#
切片只能取连续的一段。想一次取好几个不连续的位置,就要用方括号里放一个列表。
print(scores[[0, 2, 4]])[80 75 60]它和切片长得像,含义完全不同#
print(scores[1:4]) # 位置 1,2,3 —— 连续
print(scores[[0, 2, 4]]) # 位置 0,2,4 —— 不连续外层多一层方括号,这就是全部区别。
可以用布尔列表#
print(scores[[True, False, True, False, True]])[80 75 60]意思是「要第 0 个、不要第 1 个、要第 2 个……」。这其实就是布尔掩码的「手动版」。
二维数组里可以选行#
print(matrix[[0, 2]])[[1 2 3]
[7 8 9]]选「第 0 行和第 2 行」,所有列。
也可以行列一起选#
print(matrix[[0, 2], [1, 2]])[2 9]意思是:取第 0、2 行的第 1、2 列。行和列是分别配对的,不是笛卡尔积。
一个容易搞混的点:
matrix[[0, 2], [1, 2]]拿到的是[2, 9](两个数), 而不是「2 行 × 2 列」的 4 个数。 想要笛卡尔积(4 个数),要用广播——那正是第 3 章的内容:matrix[[0, 2]][:, [1, 2]] # 4 个数
知识点 4:布尔掩码(Boolean Masking)#
用条件生成一串 True/False#
mask = scores > 75
print(mask)[ True True False True False]用它来筛选#
print(scores[mask])[ 80 90 100]写成一行更常见:
print(scores[scores > 75])一维如此,二维也一样#
mask2 = matrix > 4
print(mask2)[[False False False]
[False True True]
[ True True True]]print(matrix[mask2])[5 6 7 8 9]注意结果的形状:二维数组用二维掩码筛选,返回的是一维数组(按行顺序摊平),不是二维。
布尔掩码的两个实用模式
模式一:按行筛选。 掩码要是一维的,形状跟行数一样:
print(matrix[matrix[:, 0] > 3]) # 选出所有「第 0 列大于 3」的行输出:
[[4 5 6] [7 8 9]]模式二:按元素筛选。 掩码跟原数组同形状:
matrix[matrix > 4] # 选出所有大于 4 的元素,摊平成一维这两个写法长得像,结果完全不同,是本章最容易搞混的地方。
多个条件同时满足#
用 &(并且)和 |(或者):
high = scores > 75
low = scores < 85
print(scores[high & low]) # 既大于 75 又小于 85[80]print(scores[(scores >= 80) | (scores <= 65)])[ 80 90 100 60]NumPy 里不能用 Python 的
and和or!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()口诀:数组上的逻辑运算,一律用
&|~。
取反用 ~#
print(scores[~mask]) # 不大于 75 的[75 60]知识点 5:筛选与提取(Filter and Extract)#
np.where():找出满足条件的位置#
idx = np.where(scores > 75)
print(idx)(array([0, 1, 3]),)np.where 返回的是元组,里面装着每个维度上满足条件的索引。
二维数组上:
row, col = np.where(matrix > 4)
print(row) # [1 1 2 2 2]
print(col) # [1 2 0 1 2]这告诉你每个符合条件的元素在哪儿。
np.argwhere():直接给出坐标对#
print(np.argwhere(matrix > 4))[[1 1]
[1 2]
[2 0]
[2 1]
[2 2]]比 np.where 好读——每一行就是一对坐标 [行, 列]。
三种「筛选」的分工#
| 你想要的 | 用什么 |
|---|---|
| 满足条件的元素 | arr[arr > 75] |
| 满足条件的位置 | np.where(arr > 75) 或 np.argwhere(arr > 75) |
| 满足条件的行 | arr[arr[:, 0] > 3] |
实战例子:找出哪个城市业绩最好#
sales = np.array([
[120.0, 135.0, 150.0, 165.0],
[ 80.0, 92.0, 88.0, 105.0],
[200.0, 210.0, 195.0, 230.0],
[ 60.0, 72.0, 85.0, 78.0],
[150.0, 145.0, 160.0, 175.0],
])
city_names = np.array(["北京", "上海", "广州", "深圳", "成都"])
# 每个城市的总业绩
totals = sales.sum(axis=1)
print(totals)[570. 365. 835. 295. 630.]# 最优城市的下标
best = totals.argmax()
print(best) # 2
print(city_names[best]) # 广州# 前两名的城市
order = np.argsort(totals)[::-1]
print(city_names[order[:2]])['广州' '成都']
argmax()返回的是「下标」,不是「值」。 这是新手最容易搞混的地方:totals.argmax() # 2 ← 第几个最大 totals.max() # 835.0 ← 最大值本身要拿到「是谁」,就得用下标去索引名字数组——
city_names[totals.argmax()]。
知识点 6:数组修改中的常见陷阱(Mutation Trap)#
这是 NumPy 最经典、最容易被忽略的坑。
切片给你的是「视图」#
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]原数组没变。
布尔掩码也是副本#
a = np.array([10, 20, 30, 40, 50])
part = a[a > 25]
part[0] = 999
print(a)[10 20 30 40 50]也没变。
为什么会这样?#
NumPy 把一维数组存在一块连续的内存里。切片只是「从这块内存里截一段出来看」,它不复制数据,所以叫视图(view)——改视图就是改那块内存。
而 Fancy Indexing 和布尔掩码没法用「连续的一段」来表示(位置不连续),NumPy 必须重新复制一份数据,所以叫副本(copy)。
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 ← 是副本怎么避免踩坑?#
三条路,按场景选:
# 方案 1:明确要副本,加 .copy()
part = a[1:3].copy()
# 方案 2:改完之后再赋值回去
a[1:3] = a[1:3] * 2
# 方案 3:直接用「赋值目标」的方式改
a[1:3] = [111, 222]最推荐的写法是方案 3 和方案 2。 「取出 → 修改 → 放回去」这个模式虽然看起来多写一行,但它意图明确,读代码的人一眼就知道你在改原数组。 方案 1(
.copy())适合「我就是要一份独立的副本,以后单独用」。
一个真实会出事的场景#
prices = np.array([100.0, 200.0, 300.0])
# 想把前两个打八折
discounted = prices[:2]
discounted *= 0.8 # 原地修改
print(discounted) # [80. 160.]
print(prices) # [ 80. 160. 300.] ← 原价被改了!prices 变成了 [100, 160, 300],第三个元素没动,但前两个的原价被覆盖了——这在算账的时候是灾难。
正确写法:
prices = np.array([100.0, 200.0, 300.0])
prices[:2] = prices[:2] * 0.8
print(prices) # [ 80. 160. 300.]记住这张对照表:
写法 是视图还是副本 改了会动原数组吗 a[1:3]视图 会 a[1:3, 0:2]视图 会 a[[1, 2]]副本 不会 a[a > 10]副本 不会 a.reshape(...)视图 会 a.flatten()副本 不会 a.ravel()视图(通常) 可能会 记不住也没关系——养成一个习惯:凡是要改,就写
a[...] = ...的形式。
本节小结#
- 基础索引
arr[0]、arr[-1]、arr[行, 列];二维用arr[:, 1]取列、arr[1, :]取行。 - 切片
arr[开始:结束:步长]:起点含、终点不含;二维两个维度各切各的。 - Fancy Indexing
arr[[0, 2, 4]]:方括号里多一层,取多个不连续位置。 - 布尔掩码
arr[arr > 75];多条件用&|~,不能用andor。 - 找位置:
np.where()返回索引元组,np.argwhere()返回坐标对;argmax()给的是下标不是值。 - 视图 vs 副本:切片和
reshape给视图(改了会动原数组),Fancy Indexing、布尔掩码、flatten()给副本。 - 保险写法:要改就写
a[...] = ...,要副本就写.copy()。
下一节预告 第 2.2 节用 5 道练习把上面这些练熟,特别是最后一道——「改切片结果,原数组跟着变」这个坑,看你亲手踩一遍再修好,比看十遍文字管用。