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 秒实验
- 把 L 换成 100 个整数,再比一次占用,看差距是变大还是变小。
- 把 N 改成 10000 再测一次,看倍数是不是还是 16。
- 想一想:为什么 NumPy 要求整块内存同类型?换成混类型还能不能连着读?
常见误区
- 以为数组一定比列表快:逐个取元素、频繁增删时数组反而慢。
- 混着放字符串:整列被推成文本,求和直接报错。
- 只报倍数不报环境:换台机器同一个测试可能差一倍。
- 不预热就计时:第一次跑含冷启动,测出来的数偏高。
自测三问(点开对答案)
1. 数组省内存的根本原因是什么? —— 它是一整块同类型的连续内存,没有每个元素的指针和对象头开销。 2. 数组求和为什么比列表快? —— 连续内存能被 CPU 批量读取,缓存命中率高;列表要顺着指针跳,缓存基本用不上。 3. 什么情况下该用列表而不用数组? —— 元素类型不一致,或者要频繁在中间增删元素时。知道数组是什么了。下一篇讲怎么造一个:七种造法覆盖九成场景。
- ☐ (本篇完,下一篇 6.2 创建数组的几种方式)