第 2.2 节 编程练习#
预计学习时长: 45 分钟
练习方式: 新建一个 Jupyter Notebook,把下面的数据抄进去,然后每题独立完成。第 5 题最重要,一定要亲手跑一遍。
准备:先把数据建好#
import numpy as np
scores = np.array([80, 90, 75, 100, 60])
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
])练习 1:通过索引和切片提取指定位置的元素#
题目: 完成下面 6 个任务:
- 取出
scores的第 0 个元素、第 2 个元素; - 取出
scores的最后一个元素(用负数索引); - 取出
scores位置 1 到 3 的元素(不含 3); - 取出
scores的前 3 个元素; - 取出
scores里每隔一个的元素(即下标 0、2、4); - 取出
matrix的第 1 行和第 1 列,分别打印。
思考题: 第 3 题如果用 Pandas 的 loc 写法 scores.loc[1:3],结果会一样吗?
点击查看参考答案
# 1. 单个元素
print(scores[0], scores[2]) # 80 75# 2. 负数索引
print(scores[-1]) # 60# 3. 切片:起点含、终点不含
print(scores[1:4]) # 90 75 100# 4. 前 3 个
print(scores[:3]) # 80 90 75# 5. 步长 2
print(scores[::2]) # 80 75 60# 6. 取行和取列
print(matrix[1]) # [4 5 6]
print(matrix[:, 1]) # [2 5 8]要点: 第 3 题在 Pandas 里是 df.loc[1:3] → 含 3(拿 3 个);
NumPy 里 scores[1:4] → 不含 4(拿 3 个)。想表达一样的意思,
NumPy 要写 scores[1:4],Pandas 要写 df.loc[1:3]——右边界的位置差 1。
第 6 题的冒号是关键:matrix[1] 是「第 1 行」,matrix[:, 1] 是「所有行的第 1 列」。
练习 2:使用整数数组一次性选择多个元素#
题目: 完成下面 5 个任务:
- 取出
scores里下标 0、2、4 的元素,存到picked; - 打印
picked的shape; - 用布尔列表取出同样的元素;
- 取出
matrix的第 0 行和第 2 行,打印结果; - 取出
matrix第 0、2 行的第 1、2 列,打印结果。
思考题: 第 5 题只拿到了几个数?为什么不是 4 个?
点击查看参考答案
# 1. Fancy Indexing
picked = scores[[0, 2, 4]]
print(picked)[80 75 60]# 2. shape
print(picked.shape) # (3,)# 3. 布尔列表,效果一样
print(scores[[True, False, True, False, True]])[80 75 60]# 4. 选行
print(matrix[[0, 2]])[[1 2 3]
[7 8 9]]# 5. 行和列分别配对
print(matrix[[0, 2], [1, 2]])[2 9]要点: 第 5 题只拿到 2 个数,不是 4 个。
matrix[[0, 2], [1, 2]] 的意思是「第 0 行的第 1 列」和「第 2 行的第 2 列」——行和列是一一配对的。
想要「2 行 × 2 列」的 4 个数,得用广播(第 3 章)或者先选行再选列:
print(matrix[[0, 2]][:, [1, 2]])[[2 3]
[8 9]]练习 3:使用布尔掩码筛选满足条件的元素#
题目: 完成下面 5 个任务:
- 打印
scores > 75的结果(掩码); - 用这个掩码取出大于 75 的分数;
- 一行写完上面两步;
- 取出
matrix里所有大于 4 的元素,打印结果和它的shape; - 取出
matrix里第 0 列大于 3 的那些行,打印结果。
思考题: 第 4 题和第 5 题的掩码写法只差一点点,结果却完全不同——差别在哪?
点击查看参考答案
# 1. 掩码
mask = scores > 75
print(mask)[ True True False True False]# 2. 用掩码取元素
print(scores[mask])[ 80 90 100]# 3. 一行写完
print(scores[scores > 75])[ 80 90 100]# 4. 按元素筛选(掩码和原数组同形状)
result = matrix[matrix > 4]
print(result)
print(result.shape) # (5,)[5 6 7 8 9]
(5,)# 5. 按行筛选(掩码是一维,长度 = 行数)
print(matrix[matrix[:, 0] > 3])[[4 5 6]
[7 8 9]]要点: 第 4 题和第 5 题的差别在于掩码的形状:
| 写法 | 掩码形状 | 结果 |
|---|---|---|
matrix[matrix > 4] | 和 matrix 一样(2,3) | 所有符合条件的元素,摊平成一维(5 个) |
matrix[matrix[:, 0] > 3] | 一维,长度 = 行数(3) | 符合条件的行,还是二维(2 行 3 列) |
记住:掩码是几维的,就按几维筛。
练习 4:提取满足多个条件的数组元素#
题目: 完成下面 5 个任务:
- 取出
scores中大于 75 且小于 95 的分数; - 取出
scores中大于 90 或小于 70 的分数; - 取出
scores中不在[75, 90]里的分数; - 把上面三个条件的结果都打印出来;
- 用
np.where()找出scores中大于 85 的元素位置,打印出来。
思考题: 第 3 题的 ~ 是什么意思?如果写成 not 会怎样?
点击查看参考答案
# 1. 并且
print(scores[(scores > 75) & (scores < 95)])[80 90]# 2. 或者
print(scores[(scores > 90) | (scores < 70)])[100 60]# 3. 取反:不在 [75, 90] 里
print(scores[~np.isin(scores, [75, 90])])[ 80 100 60]# 4. 三个结果一起看
print(">75 且 <95:", scores[(scores > 75) & (scores < 95)])
print(">90 或 <70:", scores[(scores > 90) | (scores < 70)])
print("不在 [75,90]:", scores[~np.isin(scores, [75, 90])])>75 且 <95: [80 90]
>90 或 <70: [100 60]
不在 [75,90]: [ 80 100 60]# 5. 找位置
print(np.where(scores > 85))(array([1, 3]),)要点: 第 3 题的 ~ 是「取反」——把 True/False 互换。
写成 not 会报 TypeError: The truth value of an array with more than one element is ambiguous,
因为 Python 的 not 只能作用在单个布尔值上。
数组上的逻辑运算一律用 & | ~。
练习 5:理解视图和副本的区别#
题目: 这是本章最重要的一题。请按顺序做完 4 组对照实验,每组都打印原数组:
- 用切片
a[1:3]取一段,把part[0]改成999,打印a; - 用Fancy Indexing
a[[1, 2]]取一段,把part[0]改成999,打印a; - 用布尔掩码
a[a > 25]取一段,把part[0]改成999,打印a; - 最后用
.copy()显式做一份副本,改它,再打印a。
思考题: 三组实验里,哪一组动了原数组?为什么?
点击查看参考答案
a = np.array([10, 20, 30, 40, 50])第 1 组:切片(视图)
a = np.array([10, 20, 30, 40, 50])
part = a[1:3]
part[0] = 999
print(a)[ 10 999 30 40 50]原数组被改了!
第 2 组:Fancy Indexing(副本)
a = np.array([10, 20, 30, 40, 50])
part = a[[1, 2]]
part[0] = 999
print(a)[10 20 30 40 50]原数组没变。
第 3 组:布尔掩码(副本)
a = np.array([10, 20, 30, 40, 50])
part = a[a > 25]
part[0] = 999
print(a)[10 20 30 40 50]原数组也没变。
第 4 组:显式拷贝
a = np.array([10, 20, 30, 40, 50])
part = a[1:3].copy()
part[0] = 999
print(a)[10 20 30 40 50]原数组还是没变。
要点: 只有切片那一组动了原数组。
原因:NumPy 把数组放在一块连续内存里。 切片只是「截一段出来看」,不复制数据,所以是视图——改视图就是改那块内存。 而 Fancy Indexing 和布尔掩码的位置不连续,没法用「一段连续内存」表示, NumPy 必须重新复制一份,所以是副本。
以后要改数组,用这个写法最保险:
a[1:3] = a[1:3] * 2 # 取出来、算、再放回去加分练习:找出「达标」的行#
题目: 用本章学的东西做一个「达标筛选」:
- 造一个 4 行的成绩表
scores_2d,每行是一个学生的 3 门课:scores_2d = np.array([ [88, 92, 75], [60, 85, 90], [95, 95, 88], [45, 60, 55], ]) - 用
axis=1算出每个学生的平均分; - 找出平均分 >= 85 的学生,用布尔掩码取出整行;
- 找出三门课都 >= 80 的学生;
- 打印这两批学生的平均分。
思考题: 第 3 题和第 4 题的掩码形状一样吗?
点击查看参考答案
import numpy as np
scores_2d = np.array([
[88, 92, 75],
[60, 85, 90],
[95, 95, 88],
[45, 60, 55],
])# 2. 每个学生的平均分
avg = scores_2d.mean(axis=1)
print(avg)[85. 78.33333333 92.66666667 53.33333333]# 3. 平均分 >= 85 的整行
good = scores_2d[avg >= 85]
print(good)[[88 92 75]
[95 95 88]]# 4. 三门课都 >= 80
all_pass = scores_2d[(scores_2d >= 80).all(axis=1)]
print(all_pass)[[95 95 88]]# 5. 两批学生的平均分
print("平均分达标:", avg[avg >= 85])
print("单科全达标:", scores_2d[(scores_2d >= 80).all(axis=1)].mean(axis=1))平均分达标: [85. 92.66666667]
单科全达标: [92.66666667]要点: 第 3 题和第 4 题的掩码形状一样(都是长度 4 的一维数组), 因为它们都是「一行一个布尔值」。
区别只在掩码怎么算出来:
- 第 3 题:
avg >= 85—— 先算出每行的平均分,再比; - 第 4 题:
(scores_2d >= 80).all(axis=1)—— 先逐元素比较,再让每一行全部为真才算达标。
.all(axis=1) 是「这一行全都满足」,.any(axis=1) 是「这一行至少一个满足」。
这两个在真实的数据筛选里非常常用。
练习小结#
| 题目 | 用到的核心语法 |
|---|---|
| 练习 1 | arr[i]、arr[-1]、arr[起:止:步]、arr[行, 列]、arr[:, 列] |
| 练习 2 | arr[[0, 2, 4]]、布尔列表选行选列 |
| 练习 3 | arr[arr > 75]、按元素筛选 vs 按行筛选 |
| 练习 4 | &、` |
| 练习 5 | 视图 vs 副本、.copy() |
做完这 5 题,下一步 现在你能从数组里精准取数了。但还有个更厉害的东西等着你——形状不同的两个数组居然能直接相乘相减,NumPy 会自动帮你「补齐」。这就是广播机制,第 3 章的内容。