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

ndarray 是「一整块同类型的数」,列表是「一串指向对象的指针」。 四个整数,列表容器占 88 字节,数组只占 32 字节。 100 万个数求和,数组比列表快 16 倍——因为它能连着读内存。

📝 本章和前面几章的关系

前五章一直在 Pandas 和 Matplotlib 上打转。它们的底座是 NumPy,这一章往下挖一层。 本机 numpy 2.5.2、python 3.13.14。所有耗时都是预热后取最快的。

一、先看长相和占用

列表 [1, 2, 3, 4] 长度 4,容器本身占 88 字节。 数组 np.array([1, 2, 3, 4]) 形状 (4,)、类型 int64、占 32 字节。 同样的四个数,差了 56 字节。而且这还没算列表里四个 int 对象本身的开销。

数组一个数就是 8 个字节,实打实的一块内存,没有指针也没有对象头。

二、类型必须统一

放整数是 int64,放小数自动变成 float64。 混着放字符串呢?整数组被推成字符串类型,连数字都变成了「字」。 试着求和,直接 TypeError——新版 numpy 连加都不让加。

类型统一是 ndarray 的地基。一旦混进一个字符串,整列都废了。 这也是为什么读 Excel 时要检查 dtype:某一列有一个「无」,整列就变文本。

三、100 万个数求和,快 16 倍

列表 7.09 毫秒,数组 0.43 毫秒。同样是求和,差了 16 倍。 原因不是「用了 C 语言」这么简单,而是内存布局。 数组是一整块连续内存,CPU 能一次取一批,缓存命中率高。

列表要顺着指针一个一个跳,每个 int 对象散落在内存各处,缓存基本用不上。 这个倍数跟机器、跟数据规模都有关。报性能必须连环境一起报,只说「快多少倍」不算数。

四、什么时候还得用列表

场景 用什么
元素类型不一样(混着放) 列表
要频繁增删中间的元素 列表
整块做同样的运算 数组
数值计算、矩阵运算 数组

数组的强项是「整块同构运算」,弱项是「灵活地增删改结构」。 要往中间插一个元素,数组比列表慢得多——它得把后面全部挪一遍。 Pandas 的每一列底下就是一个 ndarray。第 3 章学的那些,根都在这。

五、动手 Demo:初识 NumPy:数组和列表差在哪

先比四个整数的内存占用,再看混类型会变成什么,最后比 100 万个数求和。 计时统一「预热三次、跑五次取最快」,避免第一次的冷启动影响结论。

import sys
import time
import numpy as np

L = [1, 2, 3, 4]
A = np.array([1, 2, 3, 4])
print("列表 %d 字节,数组 %d 字节" % (sys.getsizeof(L), A.nbytes))
print("数组类型 %s" % A.dtype)

print("混类型:%s" % np.array([1, 2, "三"]).dtype)

N = 1000000
BIG_L = list(range(N))
BIG_A = 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: sum(BIG_L))
t2 = t(lambda: int(BIG_A.sum()))
print("列表 %.2f ms,数组 %.2f ms,快 %.0f 倍" % (t1 * 1000, t2 * 1000, t1 / t2))

实跑输出:

--------------------------------------------------------------------
初识 NumPy:数组和列表差在哪
--------------------------------------------------------------------

  ① 列表和数组,先看长相
  --------------------------------------------------------------------
  列表 [1, 2, 3, 4],长度 4,占 88 字节(含指针和对象头)。
  数组 [1 2 3 4],形状 (4,),类型 int64,占 32 字节。
  四个整数,列表光容器就要 88 字节;数组是 32 字节,一个数 8 字节。
  ! 数组省内存是因为它是「一整块同类型的数」,列表是「一串指向对象的指针」。

  ② 类型必须统一
  --------------------------------------------------------------------
  放整数就是 int64,放小数就自动变成 float64:float64。
  混着放字符串呢?<U21——整个数组被推成字符串,数字也变成字了。
  试着求和:直接报 TypeError,说类型不兼容——新版 numpy 连加都不让加。
  ! 类型统一是 ndarray 的地基。一旦混进字符串,整列都变成字符串,运算全废。

  ③ 100 万个数的求和,快多少
  --------------------------------------------------------------------
  100 万个数求和:列表 7.49 毫秒,数组 0.44 毫秒,快 17 倍。
  快的原因不是「用了 C」,而是数组是一整块内存,CPU 能连着取;
  列表要顺着指针一个一个跳,跳到哪算哪,缓存全废。
  ! 这个倍数跟机器、跟规模都有关。报性能必须连环境一起报,只说「快多少倍」不算数。

  ④ 什么时候还得用列表
  --------------------------------------------------------------------

| 场景 | 用什么 |
|---|---|
| 元素类型不一样(混着放) | 列表 |
| 要频繁增删中间的元素 | 列表 |
| 整块做同样的运算 | 数组 |
| 数值计算、矩阵运算 | 数组 |

  数组的强项是「整块同构运算」,弱项是「灵活增删改结构」。
  ! Pandas 的每一列底下就是一个 ndarray。所以第 3 章学的那些,根都在这。

  ⑤ 版本与安装
  --------------------------------------------------------------------
  本机 numpy 版本 2.5.2,python 3.13.14。
  装它一行:pip install numpy。本课用的是已经装好的,不用再装。
  ! numpy 是 Pandas、Matplotlib、scikit-learn 共同的底座,装一次全都受益。

sys.getsizeof(L) 只算容器本身,不含里面四个 int 对象的开销;数组用 nbytes 才是全部。 两边统计口径不同,这个 88 对 32 是「容器比容器」。说清口径比数字本身更要紧。

30 秒实验

  1. 把 L 换成 100 个整数,再比一次占用,看差距是变大还是变小。
  2. 把 N 改成 10000 再测一次,看倍数是不是还是 16。
  3. 想一想:为什么 NumPy 要求整块内存同类型?换成混类型还能不能连着读?

常见误区

  1. 以为数组一定比列表快:逐个取元素、频繁增删时数组反而慢。
  2. 混着放字符串:整列被推成文本,求和直接报错。
  3. 只报倍数不报环境:换台机器同一个测试可能差一倍。
  4. 不预热就计时:第一次跑含冷启动,测出来的数偏高。
自测三问(点开对答案) 1. 数组省内存的根本原因是什么? —— 它是一整块同类型的连续内存,没有每个元素的指针和对象头开销。 2. 数组求和为什么比列表快? —— 连续内存能被 CPU 批量读取,缓存命中率高;列表要顺着指针跳,缓存基本用不上。 3. 什么情况下该用列表而不用数组? —— 元素类型不一致,或者要频繁在中间增删元素时。

知道数组是什么了。下一篇讲怎么造一个:七种造法覆盖九成场景。

相关笔记