📊 第6章 图解数组计算模块 NumPy
📄 一句话概括

reshape 只改「怎么看」,不改数据,新数组和原数组共享内存。 reshape 里写 -1,让 NumPy 自己算那一维,一行最多一个 -1。 append 每次都新建整个数组,循环里用它就是灾难。

📝 本章和前面几章的关系

这一篇前半段讲 reshape、转置、展平,后半段讲增删改查。 共同点是:它们几乎都返回新数组,原数组不动。唯一例外是写视图。

一、reshape:只改看法,不改数据

12 个元素的一维数组,形状是 (12,)。 reshape(3, 4) 之后形状变成 (3, 4),元素个数还是 12。 它不改数据本身,只改「怎么看它」。内存里还是那 12 个数。

所以 B 和 A 共享同一块内存。B[0, 0] = 99 之后,A 的第 0 个也变成 99。 元素总数必须对得上。12 个元素只能塑成 1×12、2×6、3×4、4×3、6×2、12×1。 对不上就报错,不会帮你补 0 或者截掉。

二、-1 让 NumPy 自己算

A.reshape(3, -1) 给形状 (3, 4),那个 4 是它自己算出来的。 A.reshape(-1, 6) 给 (2, 6),它算出 2。 一行里最多只能有一个 -1。两个 -1 它没法唯一确定,直接报错。

最常用的是 reshape(-1, 1),把一维数组竖成一列。 做矩阵乘法、拼 DataFrame 的时候,行向量和列向量不是一个东西,这一步躲不开。

三、转置与展平

2 行 3 列的矩阵,.T 之后是 3 行 2 列,行变列、列变行。 转置对二维是翻转,对一维没效果——一维没有行列可分。 展平有两个函数:ravel() 和 flatten(),结果都是 6 个数排成一行。

差别在返回什么。ravel 可能返回视图,flatten 一定返回复制。 省内存用 ravel,要改又要安全用 flatten。名字不好记就记「flatten 更笨更安全」。

四、增删:append 每次都新建

np.append(A, 4) 给 [1 2 3 4],np.delete(A, 1) 删掉第 2 个。 看着像原地修改,其实每次都新建一个数组,原数组 A 一个字都没变。 所以循环里 append 是灾难:每加一个元素,就要把前面所有元素复制一遍。

1000 次 append 大约要复制 50 万次元素。这和 Pandas 里逐行 concat 是同一个毛病。 NumPy 没有原地插入。真要频繁增删,用 Python 列表,最后再一次性转数组。

五、改与查:where

np.where(条件) 只给一个参数时,返回满足条件的下标。 np.where(条件, 值1, 值2) 给三个参数时,条件真取值 1,假取值 2。 所以「把不及格的改成 60」写成 np.where(A < 60, 60, A)。

注意参数顺序:先写真值再写假值,不是先旧值后新值。这里最容易写反。 where 返回新数组,不改原数组。它和 Pandas 里的 mask 是同一件事。

六、动手 Demo:重塑与增、删、改、查

先验证 reshape 和原数组共享内存,再看 -1 怎么自动算。 然后转置、展平、append、where,每步都打出来看形状。

import numpy as np

A = np.arange(12)
print("原数组 %d 个元素,形状 %s" % (A.size, A.shape))
B = A.reshape(3, 4)
print("reshape 后形状 %s,元素还是 %d" % (B.shape, B.size))
print(B)
B[0, 0] = 99
print("改 B 的第一个,A 变成 %s" % A[:4])

A = np.arange(12)
print("reshape(3, -1) 形状 %s" % (A.reshape(3, -1).shape,))
print("reshape(-1, 6) 形状 %s" % (A.reshape(-1, 6).shape,))

M = np.arange(1, 7).reshape(2, 3)
print("2 行 3 列:")
print(M)
print("转置形状 %s:" % (M.T.shape,))
print(M.T)
print("ravel  %s" % M.ravel())
print("flatten %s" % M.flatten())

A = np.array([1, 2, 3])
print("append 一个 4:%s" % np.append(A, 4))
print("append 两个:%s" % np.append(A, [4, 5]))
print("delete 第 2 个:%s" % np.delete(A, 1))
print("原数组还是 %s" % A)

S = np.array([88, 92, 75, 85, 60])
print("S > 80 的位置:%s" % np.where(S > 80)[0])
print("不满足的填 0:%s" % np.where(S > 80, S, 0))
print("低于 60 的改成 60:%s" % np.where(S < 60, 60, S))

实跑输出:

--------------------------------------------------------------------
重塑与增、删、改、查
--------------------------------------------------------------------

  ① reshape:只改看法,不改数据
  --------------------------------------------------------------------
  原数组 12 个元素,形状 (12,)。
  reshape(3, 4) 之后形状 (3, 4),元素个数还是 12。
  [0, 1, 2, 3]
  [4, 5, 6, 7]
  [8, 9, 10, 11]
  reshape 不改数据本身,只改「怎么看它」。B 和 A 共享同一块内存。
  改 B 的第一个,A 的第 0 个也跟着变了:[99  1  2  3]
  ! 元素总数必须对得上。12 个元素只能重塑成 1×12、2×6、3×4、4×3、6×2、12×1。

  ② -1 让 NumPy 自己算
  --------------------------------------------------------------------
  reshape(3, -1) → 形状 (3, 4),它自己算出 4。
  reshape(-1, 6) → 形状 (2, 6),它自己算出 2。
  一行里最多只能有一个 -1。两个 -1 它没法算。
  ! -1 是「这一维你替我算」。写 reshape(-1, 1) 把一维变成列向量,这个最常用。

  ③ 转置与展平
  --------------------------------------------------------------------
  2 行 3 列:
  [1, 2, 3]
  [4, 5, 6]
  转置 .T 之后形状 (3, 2):
  [1, 4]
  [2, 5]
  [3, 6]
  展平 ravel 是视图,flatten 是复制:[1 2 3 4 5 6] / [1 2 3 4 5 6]
  ! ravel 可能返回视图,flatten 一定返回复制。要改就用 flatten,要省内存用 ravel。

  ④ 增删:append 和 delete
  --------------------------------------------------------------------
  append 一个 4:[1 2 3 4]
  append 整个数组:[1 2 3 4 5]
  delete 第 1 个:[1 3]
  append 每次都新建一个数组,不改原数组。原数组 A 还是 [1 2 3]。
  ! 循环里 append 是灾难:每次都要复制整个数组,和 Pandas 的逐行 concat 一个道理。

  ⑤ 改与查:where
  --------------------------------------------------------------------
  where(条件) 给位置:A > 80 的位置是 [0 1 3]
  where(条件, 值1, 值2) 给替换:[88 92  0 85  0]
  把不及格的改成 60:[88 92 75 85 60]
  where 返回新数组,不改原数组。
  ! where 是「按条件换值」的标准写法,等价于 Pandas 里的 mask。

最后一行打出来和原数组一模一样。因为 S 里最低就是 60,一个都没被改到。 「跑了但没变化」也是一种信号:先确认条件真的命中了,再怀疑写法。 append 那三行能看出原数组一直是 [1 2 3],这就是「每次都新建」的直接证据。

30 秒实验

  1. 把 np.where(S < 60, 60, S) 里的 60 改成 70,看这次哪些数被改了。
  2. 把 reshape(3, -1) 改成 reshape(5, -1),看报什么错。
  3. 想一想:为什么 NumPy 不支持原地插入?这和它「一整块连续内存」的设计有什么关系?

常见误区

  1. 以为 reshape 复制了数据:它只改看法,改新数组会连带改原数组。
  2. 写两个 -1:没法唯一确定,直接报错。
  3. 循环里 append:每次都复制整个数组,数据一大就卡死。
  4. where 三个参数的顺序写反:先真值后假值,写反了逻辑正好颠倒。
自测三问(点开对答案) 1. reshape 之后,新数组和原数组是什么关系? —— 共享同一块内存,改新数组会改动原数组。它就是同一份数据的另一种看法。 2. ravel 和 flatten 的区别? —— ravel 可能返回视图,flatten 一定返回复制。要改就用 flatten。 3. np.where(A < 60, 60, A) 是什么意思? —— 条件真的位置取 60,假的保持 A 原值。也就是把低于 60 的抬到 60。

形状和内容都能改了。下一篇讲矩阵:星号和 @ 到底差在哪。

相关笔记