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

数组里每个元素占一样多的字节,类型统一是它省内存的根本。 astype 返回新数组,不接住返回值等于没改。 星号是逐元素乘,矩阵乘要写 @。这一处搞错,结果完全是两回事。

📝 本章和前面几章的关系

上一篇讲怎么造数组。这一篇讲数组里装的是什么,以及能对它做什么运算。 本机 numpy 2.5.2、python 3.13.14,耗时统一预热后取最快。

一、三个属性看清一个数组

A.dtype 是类型,A.itemsize 是每个元素占几字节,A.nbytes 是整块占多少。 np.array([1, 2, 3]) 的 dtype 是 int64,itemsize 8,nbytes 24。 三者是乘出来的关系:24 等于 8 乘 3。不是三个互相独立的数。

int64 的意思是每个整数占 64 位,也就是 8 个字节,能存的范围很大。 小一号的 int8 只占 1 个字节,范围是 -128 到 127。

二、装不下的时候,新版直接报错

往 int8 里塞 200,numpy 2.5 直接抛 OverflowError。 因为 200 超出了 int8 的上限 127,它装不下。 老版本不报错,会静默溢出成 -56。200 减 256 等于 -56,绕回去了。

报错顶多让程序停下来,静默溢出是让错的那个数继续往下算。 后者危险得多。等你发现结果不对,已经不知道是哪个环节转错的。 所以读数据之后先看 dtype,别等到算出一堆怪数才回头查。

三、astype:换类型要接住返回值

A.astype(float) 把整数转成浮点,打出来每个数后面多一个小数点。 但它返回的是新数组,原数组一个字都没动。 必须写 A = A.astype(float)。只写前半句,跑了也白跑。

这条和 Pandas 的 astype、replace、dropna 是一套规矩:默认不原地改。 判断标准很简单:函数有没有返回东西。返回了,就得接住。

四、逐元素运算,还有快 55 倍的向量化

A 是 [1, 2, 3],B 是 [10, 20, 30]。 A + B 给 [11 22 33],A * B 给 [10 40 90],B / A 给 [10. 10. 10.]。 位置对位置,一个算一个。除法结果自动变成浮点,不会截成整数。

A * B 是逐元素乘,不是矩阵乘。矩阵乘要写 A @ B,下一篇专门讲。 100 万个数乘 2:列表推导 118.8 毫秒,数组 2.1 毫秒,快 55 倍。 数组不用写循环,一行乘法就整块算完。这叫向量化。

向量化不只是代码短,是真的快。把循环交给底层,绕开 Python 一层层的解释开销。

五、广播:形状不一样也能算

2 行 3 列的矩阵加一个数 100,每行都变成 [101, 102, 103] 和 [104, 105, 106]。 加一个长度 3 的数组 [10, 20, 30],第一行变 [11, 22, 33],第二行变 [14, 25, 36]。 广播的规则:从最后一维往前对,长度相等,或者其中一个是 1。

对不上就报错,不会凑合算。原文是 operands could not be broadcast together。 2 行 3 列加长度 2 的数组就报这个错,因为最后一维 3 和 2 对不上。 报错时先看两边形状,从最后一维往前对一遍,问题立刻现形。

六、动手 Demo:数据类型与数组运算

先看 dtype、itemsize、nbytes 三个属性怎么互相对上,再试塞爆 int8。 然后跑逐元素运算、100 万个数乘 2 的对比,最后看广播和对不上的报错。

import time
import numpy as np

A = np.array([1, 2, 3])
print("dtype %s,每元素 %d 字节,整块 %d 字节" % (A.dtype, A.itemsize, A.nbytes))
print("转成 float:%s" % A.astype(float))
print("原数组还是 %s" % A)

try:
    np.array([200], dtype=np.int8)
except OverflowError:
    print("塞 200 进 int8:OverflowError,上限 127")

A = np.array([1, 2, 3])
B = np.array([10, 20, 30])
print("A + B = %s" % (A + B))
print("A * B = %s(逐元素)" % (A * B))
print("A 的平方 = %s" % np.square(A))
print("B / A = %s" % (B / A))

N = 1000000
L = list(range(N))
BIG = np.arange(N)

def t(fn):
    for _ in range(3):
        fn()
    best = 1e9
    for _ in range(5):
        s = time.perf_counter()
        fn()
        best = min(best, time.perf_counter() - s)
    return best

t1 = t(lambda: [x * 2 for x in L])
t2 = t(lambda: BIG * 2)
print("100 万数乘 2:列表 %.1f ms,数组 %.1f ms,快 %.0f 倍"
      % (t1 * 1000, t2 * 1000, t1 / t2))

M = np.array([[1, 2, 3], [4, 5, 6]])
print("加一个数:")
print(M + 100)
print("加长度 3 的数组:")
print(M + np.array([10, 20, 30]))

try:
    M + np.array([1, 2])
except ValueError as e:
    print("加长度 2 的数组:%s" % str(e).split(chr(10))[0][:70])

实跑输出:

--------------------------------------------------------------------
数据类型与数组运算
--------------------------------------------------------------------

  ① 类型与查看
  --------------------------------------------------------------------
  [1 2 3] 的 dtype 是 int64,itemsize 8 字节,nbytes 24 字节。
  转成 float:[1. 2. 3.]
  想塞 200 进 int8:直接 OverflowError。int8 上限 127。
  新版 numpy 直接报错;老版本会静默溢出成 -56(200 减 256)。
  静默溢出比报错危险得多——错的那一位就这样混进后面的计算里。
  ! astype 是复制,不是原地改。要生效得接住返回值。

  ② 逐元素运算
  --------------------------------------------------------------------
  A + B = [11 22 33]
  A * B = [10 40 90](逐元素相乘,不是点积)
  A 的平方 = [1 4 9]
  B / A = [10. 10. 10.]
  ! 星号是逐元素乘,矩阵乘法要写 @ 或 dot。这是 NumPy 最容易错的一处。

  ③ 和列表比:省掉循环
  --------------------------------------------------------------------
  100 万个数乘 2:列表推导 118.8 毫秒,数组 2.1 毫秒,快 55 倍。
  数组不用写循环,一行 A * 2 就整块算完。这叫「向量化」。
  ! 向量化不只是短,是真的快。同样的算法,绕开 Python 的循环就赢一大截。

  ④ 广播:形状不一样也能算
  --------------------------------------------------------------------
  2 行 3 列的矩阵,加一个数:
  [101, 102, 103]
  [104, 105, 106]
  加一个长度 3 的数组:
  [11, 22, 33]
  [14, 25, 36]
  广播的规则:从最后一维往前对,能对上就铺开。对不上报错。
  ! 广播让「每行减自己的均值」这类操作不用写循环:A 减 A.mean(axis=1, keepdims=True)。

  ⑤ 广播对不上会怎样
  --------------------------------------------------------------------
  2 行 3 列加长度 2 的数组:报 ValueError。
  原文:operands could not be broadcast together with shapes (2,3) (2,)
  规则对不上就报错,不会「凑合算」。这是好事——错了马上知道。
  ! 报错时先看两边形状:从最后一维往前对,长度要么相等,要么其中一个是 1。

np.square(A) 就是 A 的平方,写法比双星号运算符更安全,不会被当成加粗标记。 int8 的溢出这一条要记牢:新版本报错,老版本静默给出 -56。报错比给出错数好。 55 倍这个数只对「100 万个整数乘 2」这一件事成立,换运算换规模都会变。

30 秒实验

  1. 把 numpy 版本、机器都没变的情况下,把 N 改成 100 万再改成 10 万,看倍数怎么变。
  2. 把 A 改成 int8 数组,再往里面加 100,看会不会溢出。
  3. 想一想:广播为什么允许「其中一个是 1」?它把那一维当成重复了几遍?

常见误区

  1. 以为 A * B 是矩阵乘:它只是逐元素乘,矩阵乘要写 @。
  2. 不接 astype 的返回值:转完类型就丢了,原数组根本没变。
  3. 看到 dtype 是 object 不当回事:多半是混进了字符串或空值,后面算不动。
  4. 以为溢出会报错:新版本会,老版本不会,会把错数悄悄带下去。
自测三问(点开对答案) 1. nbytes、itemsize、元素个数三者是什么关系? —— nbytes 等于 itemsize 乘元素个数。8 乘 3 得 24。 2. 往 int8 里塞 200,新版和老版分别怎样? —— 新版抛 OverflowError;老版静默溢出成 -56,也就是 200 减 256。 3. 广播对不上时,第一步该看什么? —— 看两边的形状,从最后一维往前对,长度要么相等,要么其中一个是 1。

运算会了,接下来是怎么把要的那部分取出来。下一篇:索引与切片,有一个视图的坑。

相关笔记