Python 列表什么都能装,代价是每个元素都是一个完整对象——整数 42 在列表里不是 4 字节,而是 28 字节起步。当你只需要存一串同类型数值,array 模块用 C 连续内存把开销砍到原生的字节宽度。这篇文章把类型码、缓冲协议、可变序列行为和与列表的取舍一次讲透。
类型码:一行声明锁定内存布局
array.array 的第一个参数是类型码(type code),它决定了每个元素占多少字节、能存什么范围。常见类型码:
| 类型码 | C 类型 | 最小字节 | Python 说明 |
|---|---|---|---|
'b' |
signed char | 1 | -128 ~ 127 |
'B' |
unsigned char | 1 | 0 ~ 255 |
'h' |
signed short | 2 | -32768 ~ 32767 |
'i' |
signed int | 4 | 平台相关 |
'f' |
float | 4 | 单精度浮点 |
'd' |
double | 8 | 双精度浮点 |
选错类型码,数据会溢出或截断:
import array
a = array.array('b', [127, 128]) # 128 超出 signed char 范围
# OverflowError: signed integer is greater than maximum
实际使用时,先确认你的数值范围,再选最小的能容纳的类型码——这是 array 省内存的第一道门。
可变序列行为:列表的接口,紧凑的底层
array.array 实现了 MutableSequence 的全部接口:append、extend、insert、pop、remove、切片赋值、reverse、index。你用列表的习惯基本可以直接搬过来,但有两处行为差异值得注意。
切片返回的仍是 array,不是列表:
import array
a = array.array('i', range(10))
s = a[2:5]
print(type(s)) # <class 'array.array'>
print(s) # array('i', [2, 3, 4])
切片赋值会强制类型检查:
a[2:5] = [10, 20, 30] # OK,全是整数
a[2:5] = [10, 20, 3.14] # TypeError: integer argument expected, got float
列表切片赋值什么类型都能塞,array 不行——类型码是契约,赋值时必须遵守。这既是约束,也是保障:你永远不会在遍历时突然撞上一个类型不对的元素。
缓冲协议:零拷贝对接 C 扩展和二进制 I/O
array 实现了 Python 的缓冲协议(buffer protocol),这意味着它的底层连续内存可以被其他支持该协议的对象直接访问,不需要先转成字节串再拷贝一遍。
直接写入二进制文件:
import array
a = array.array('f', [1.0, 2.5, 3.7])
with open('floats.bin', 'wb') as f:
a.tofile(f) # 直接把内存内容写入文件,零转换
# 读回来
b = array.array('f')
with open('floats.bin', 'rb') as f:
b.fromfile(f, 3) # 3 是元素个数,不是字节个数
print(b) # array('f', [1.0, 2.5, 3.7])
与 struct 模块协作解析二进制协议:
import array, struct
# 模拟一段网络包:2 字节 short + 4 字节 int + 4 字节 float
raw = struct.pack('>hif', 100, 200000, 3.14)
header = array.array('h', raw[:2]) # 直接从 buffer 切片构造
print(header) # array('h', [100])
与 NumPy 共享内存:
import array
import numpy as np
a = array.array('d', [1.1, 2.2, 3.3])
nd = np.frombuffer(a, dtype=np.float64) # 共享底层 buffer,不拷贝
print(nd) # [1.1 2.2 3.3]
nd[0] = 99.0
print(a[0]) # 99.0 —— 改了 NumPy 视图,array 也变了
缓冲协议是 array 最大的隐藏优势:它让 Python 代码和 C 扩展、文件 I/O、NumPy 之间形成零拷贝的数据通道,而列表做不到这一点。
实战:用 array 替换列表处理百万级数值
下面是一个可直接运行的对比脚本,测量存储开销和遍历速度:
import array
import sys
import timeit
N = 1_000_000
# --- 列表 ---
lst = list(range(N))
lst_mem = sys.getsizeof(lst) + sum(sys.getsizeof(x) for x in lst[:1000]) * N // 1000
# 粗估:列表对象 ~8MB + 每个整数对象 ~28B ≈ 36MB
# --- array ---
arr = array.array('i', range(N))
arr_mem = sys.getsizeof(arr) # 只含连续 C 内存 + 小额对象头
print(f"列表估算内存: ~{lst_mem / 1024 / 1024:.1f} MB")
print(f"array 实际内存: {arr_mem / 1024 / 1024:.1f} MB")
# --- 遍历计时 ---
lst_time = timeit.timeit('sum(lst, 0)', globals=globals(), number=10)
arr_time = timeit.timeit('sum(arr, 0)', globals=globals(), number=10)
print(f"列表求和 10 次: {lst_time:.3f}s")
print(f"array 求和 10 次: {arr_time:.3f}s")
典型输出(macOS,Python 3.12):
列表估算内存: ~36.0 MB
array 实际内存: 4.0 MB
列表求和 10 次: 0.82s
array 求和 10 次: 0.91s
内存砍到 1/9,但纯 Python 遍历反而略慢——因为 array 的元素每次取出都要临时构造一个 Python int 对象。array 的速度优势不在 Python 层遍历,而在缓冲协议的零拷贝通道和文件 I/O。 如果需要快速数值计算,应该把 array 通过 np.frombuffer 交给 NumPy 处理。
什么时候用 array,什么时候别用
| 场景 | 推荐 |
|---|---|
| 存百万级同类型数值,内存敏感 | array |
| 需要直接读写二进制文件或与 C/NumPy 共享内存 | array |
| 网络包解析、传感器数据流、音频采样缓冲区 | array |
| 元素类型混合(整数 + 字符串 + 对象) | list |
| 需要频繁插入/删除中间元素 | list(array 的 insert 是 O(n) 且无优化) |
| 需要大量数值运算(求均值、矩阵变换) | NumPy,array 只当搬运工 |
决策清单:
- 数据是否全部是同一数值类型?→ 否则直接用列表。
- 数据量是否超过十万级且内存有压力?→ array 值得考虑。
- 是否需要与二进制 I/O 或 C 扩展零拷贝对接?→ array 是最轻量的选择。
- 是否要做数学运算?→ 用 array 做存储,运算交给 NumPy。
- 是否需要嵌套结构或字典式访问?→ array 不支持,回到列表或自定义类。
array 不是列表的替代品,而是列表在数值密集场景下的专用压缩形态。理解类型码和缓冲协议,你就能在合适的场景把内存和 I/O 开销同时压到最低。