返回首页

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

第 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 个任务:

  1. 取出 scores 的第 0 个元素、第 2 个元素;
  2. 取出 scores 的最后一个元素(用负数索引);
  3. 取出 scores 位置 1 到 3 的元素(不含 3);
  4. 取出 scores 的前 3 个元素;
  5. 取出 scores 里每隔一个的元素(即下标 0、2、4);
  6. 取出 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 个任务:

  1. 取出 scores 里下标 0、2、4 的元素,存到 picked;
  2. 打印 picked 的 shape;
  3. 用布尔列表取出同样的元素;
  4. 取出 matrix 的第 0 行和第 2 行,打印结果;
  5. 取出 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 个任务:

  1. 打印 scores > 75 的结果(掩码);
  2. 用这个掩码取出大于 75 的分数;
  3. 一行写完上面两步;
  4. 取出 matrix 里所有大于 4 的元素,打印结果和它的 shape;
  5. 取出 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 个任务:

  1. 取出 scores 中大于 75 且小于 95 的分数;
  2. 取出 scores 中大于 90 或小于 70 的分数;
  3. 取出 scores 中不在 [75, 90] 里的分数;
  4. 把上面三个条件的结果都打印出来;
  5. 用 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 组对照实验,每组都打印原数组:

  1. 用切片 a[1:3] 取一段,把 part[0] 改成 999,打印 a;
  2. 用Fancy Indexing a[[1, 2]] 取一段,把 part[0] 改成 999,打印 a;
  3. 用布尔掩码 a[a > 25] 取一段,把 part[0] 改成 999,打印 a;
  4. 最后用 .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     # 取出来、算、再放回去

加分练习:找出「达标」的行#

题目: 用本章学的东西做一个「达标筛选」:

  1. 造一个 4 行的成绩表 scores_2d,每行是一个学生的 3 门课:
    scores_2d = np.array([
        [88, 92, 75],
        [60, 85, 90],
        [95, 95, 88],
        [45, 60, 55],
    ])
  2. 用 axis=1 算出每个学生的平均分;
  3. 找出平均分 >= 85 的学生,用布尔掩码取出整行;
  4. 找出三门课都 >= 80 的学生;
  5. 打印这两批学生的平均分。

思考题: 第 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) 是「这一行至少一个满足」。 这两个在真实的数据筛选里非常常用。


练习小结#

题目用到的核心语法
练习 1arr[i]、arr[-1]、arr[起:止:步]、arr[行, 列]、arr[:, 列]
练习 2arr[[0, 2, 4]]、布尔列表选行选列
练习 3arr[arr > 75]、按元素筛选 vs 按行筛选
练习 4&、`
练习 5视图 vs 副本、.copy()

做完这 5 题,下一步 现在你能从数组里精准取数了。但还有个更厉害的东西等着你——形状不同的两个数组居然能直接相乘相减,NumPy 会自动帮你「补齐」。这就是广播机制,第 3 章的内容。

阅读 --