返回首页

Roadmap
更新于 2026年10月25日 全文 1711 字未阅读

第 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 列
2
print(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];多条件用 & | ~,不能用 and or。
  • 找位置:np.where() 返回索引元组,np.argwhere() 返回坐标对;argmax() 给的是下标不是值。
  • 视图 vs 副本:切片和 reshape 给视图(改了会动原数组),Fancy Indexing、布尔掩码、flatten() 给副本。
  • 保险写法:要改就写 a[...] = ...,要副本就写 .copy()。

下一节预告 第 2.2 节用 5 道练习把上面这些练熟,特别是最后一道——「改切片结果,原数组跟着变」这个坑,看你亲手踩一遍再修好,比看十遍文字管用。

阅读 --