Python array 模块:用类型码和缓冲协议榨干数值存储的性能

2026-05-15 43 预计阅读时间:1 分钟
来源:realpython.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:8 分钟

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 的全部接口:appendextendinsertpopremove、切片赋值、reverseindex。你用列表的习惯基本可以直接搬过来,但有两处行为差异值得注意。

切片返回的仍是 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 只当搬运工

决策清单:

  1. 数据是否全部是同一数值类型?→ 否则直接用列表。
  2. 数据量是否超过十万级且内存有压力?→ array 值得考虑。
  3. 是否需要与二进制 I/O 或 C 扩展零拷贝对接?→ array 是最轻量的选择。
  4. 是否要做数学运算?→ 用 array 做存储,运算交给 NumPy。
  5. 是否需要嵌套结构或字典式访问?→ array 不支持,回到列表或自定义类。

array 不是列表的替代品,而是列表在数值密集场景下的专用压缩形态。理解类型码和缓冲协议,你就能在合适的场景把内存和 I/O 开销同时压到最低。


相关推荐