CPython array 模块深入解析:类型化数值数组的高效内存表示与完整 API 指南
array 模块(标准库内置于 CPython)提供一种以紧凑、连续内存存储同类型基本数值(字符、整数、浮点数、复数)的可变序列类型 array.array。本文以 Doc/library/array.rst 为主干,结合 Modules/arraymodule.c 的底层实现与 Lib/test/test_array.py 的测试用例,系统讲解类型码全表、构造与序列语义、读写/转换/字节序处理等全部 API,读完即可在数据文件解析、内存紧凑存储、与 C 扩展桥接等场景中直接落地使用。
一、array 是什么:与 list 同构、但元素被类型约束
array.array 是一个可变的 sequence 类型,行为非常接近 list,唯一的根本区别在于:存储在其中的对象类型在创建时就被类型码(type code)锁定,同一数组内只能保存该类型的值。C 语言实现头部的注释给出了最精炼的定义:
"An array is a uniform list -- all items have the same type. The item type is restricted to simple C types like int or float"(见 Modules/arraymodule.c)。
在 Modules/arraymodule.c 中,数组对象的核心结构体 arrayobject 除了标准的 PyObject_VAR_HEAD 长度头部之外,主要包含四个字段:
char *ob_item—— 一段连续分配的内存,真正存放所有元素;Py_ssize_t allocated—— 已分配容量;const struct arraydescr *ob_descr—— 指向描述类型码信息的描述符;Py_ssize_t ob_exports—— 导出 buffer 的次数(用于内存视图引用计数)。
由于元素在内存中紧密排列且大小固定,相同数量元素下 array 比存 int/float 对象的 list 显著节省内存;同时也因为内存布局连续,天然适合读写二进制数据、与 C 层做零拷贝交互。测试 Lib/test/test_array.py 也确认 array.array("B") 属于 collections.abc.MutableSequence 与 Reversible,即它完整地实现了标准可变序列协议。
由于浮点与整数在 Python 层都是盒子对象,而 array 把原始字节直接存在 ob_item 里,因此对于百万级元素来说,其内存开销远小于 list——这是它最典型的使用动机。
二、类型码全表:typecode 决定元素 C 类型、Python 类型与最小字节数
类型的指定发生在对象创建时。类型码与底层 C 类型、暴露给 Python 的类型以及最小字节数的对应关系如下表(出自 Doc/library/array.rst):
| 类型码 | C 类型 | Python 类型 | 最小字节数 | 备注 |
|---|---|---|---|---|
'b' |
signed char | int | 1 | |
'B' |
unsigned char | int | 1 | |
'w' |
Py_UCS4 | Unicode 字符 | 4 | (1) |
'h' |
signed short | int | 2 | |
'H' |
unsigned short | int | 2 | |
'i' |
signed int | int | 2 | |
'I' |
unsigned int | int | 2 | |
'l' |
signed long | int | 4 | |
'L' |
unsigned long | int | 4 | |
'q' |
signed long long | int | 8 | |
'Q' |
unsigned long long | int | 8 | |
'e' |
_Float16 | float | 2 | (2) |
'f' |
float | float | 4 | |
'd' |
double | float | 8 | |
'Zf' |
float complex | complex | 8 | (3) |
'Zd' |
double complex | complex | 16 | (3) |
三个备注的含义需要特别说明:
'w'(Unicode 字符):Python 3.13 新增(versionadded:: 3.13)。其 C 类型为Py_UCS4,即一个码点占 4 字节,与list[str]相比内存更紧凑。'e'(半精度浮点):Python 3.15 新增。对应 IEEE 754 2008 修订版引入的 binary16"半精度"类型。文档明确指出该类型并未被多数 C 编译器广泛支持,只有在编译器支持 C23 标准 Annex H 时才以_Float16形式可用。在 Modules/arraymodule.c 中它由描述符{"e", sizeof(short), e_getitem, e_setitem, NULL, 0, 0}定义——注意其itemsize为sizeof(short)(2 字节),且没有 compare 回调。'Zf'/'Zd'(复数):Python 3.15 新增,且文档明确它们是无条件可用的,不依赖 C 编译器对 C11 Annex G(复数)的支持。按 C11 规定,每个复数类型由"实部、虚部"两元素的 C 数组表示,因此描述符中Zf为2*sizeof(float)、Zd为2*sizeof(double)(见 Modules/arraymodule.c)。
关于表中"最小字节数"栏需要留心一点:表中的数值只是下限,实际尺寸由机器架构(严格说是 C 实现)决定,例如 'i' 在文档标注 2 字节但在现代 32/64 位平台通常是 4 字节,'l' 在 Windows 与 Linux 上的位宽也不同。因此不要以表推断内存布局,而要通过 array.itemsize 属性获取运行时的真实值。可通过模块级属性一次性查看所有可用类型码:
import array
array.typecodes # 例如 ('b','B','w','h','H','i','I','l','L','q','Q','e','f','d','Zf','Zd')
typecodes 是"所有可用类型码组成的元组"。需要留意版本差异:Python 3.15 之前它是 str(versionchanged:: 3.15 记录其从 str 变为 tuple)。测试 Lib/test/test_array.py 对每个类型码逐一验证了它们是 tuple 中长度 ≥ 1 的 str 元素。
类型码的既有生态提醒:
ctypes、struct以及第三方库(如 NumPy)使用相似但不完全相同的类型码体系,跨库转换时务必核对各家格式字符。相关背景可对照 Doc/library/struct.rst 与 Doc/library/ctypes.rst。
三、构造与初始化:typecode 必选、initializer 可选
class array(typecode[, initializer])
typecode 是唯一必选参数;可选的 initializer 必须满足以下三类情形之一,否则抛 TypeError:
bytes或bytearray对象:整个 initializer 会被传给新数组的frombytes()方法(按机器值逐字节解释,见下文)。- Unicode 字符串:会被传给
fromunicode()方法——只对'w'数组合法,否则抛ValueError。 - 其它一切可迭代对象:其迭代器被传给
extend()方法逐项追加,因此列表、range、生成器乃至另一个 array 均可作 initializer。
官方示例给出了几种典型形态(Doc/library/array.rst):
array('l')
array('w', 'hello \u2641')
array('l', [1, 2, 3, 4, 5])
array('d', [1.0, 2.0, 3.14, -inf, nan])
注意最后一行浮点数组里直接使用了 -inf 与 nan 这样的名字——这并非语法糖,而是与下一节"字符串表示可被 eval 还原"的保证相关。
3.1 支持可变序列的全部常规操作
Array 对象支持 typing 层面标注的可变序列常规操作:索引、切片、拼接与乘法(Doc/library/array.rst)。但有两条类型约束必须牢记:
- 切片赋值时,右侧必须是同类型码的 array 对象,其它类型一律抛
TypeError; - 元素赋值、
extend等操作要求元素类型可被转换为数组的类型码。
空数组的边界行为(切片自赋值、+、*)在 Lib/test/test_array.py 的 test_empty 中有覆盖,可作为参考。
3.2 泛型标注支持
Array 是对其元素内容泛型化的(versionchanged 相关说明见 Doc/library/array.rst)。在 Modules/arraymodule.c 中,array.__class_getitem__ 被注册为 Py_GenericAlias,并附文档字符串 "Arrays are generic over the type of their elements"。这意味着可以做如下静态标注:
from array import array
def load_samples() -> array[int]: # 或 array[float]、array['d'] 风格的运行时别名
...
3.3 审计事件
构造数组会触发 array.__new__ 审计事件,钩子参数为 (typecode, initializer)(见 Doc/library/array.rst)。在用 sys.addaudithook 做安全审计的环境中,可通过订阅该事件观察数组的创建来源。
四、两个核心实例属性
array.typecode
创建数组时所用的类型码字符(单个字符,如 'd';复数类型为双字符 'Zf'/'Zd')。
array.itemsize
内部表示中单个元素占用的字节数。如前所述,机器架构不同导致 'i'/'l' 等类型码的实际 itemsize 可能大于类型码表的最小字节数;同时注意 'e' 的 itemsize 为 2、'Zf' 为 8、'Zd' 为 16(见 Modules/arraymodule.c 描述符表)。二者配合即可推算缓冲总字节数。
五、就地修改类方法
append(value, /)
把 value 追加到数组末尾。位置参数只支持位置传参(/ 表示其后参数不可作关键字),元素类型不匹配时抛 TypeError。
extend(iterable, /)
把可迭代对象中的元素追加到数组末尾,两条约束(Doc/library/array.rst):
- 若
iterable是另一个 array,其类型码必须与当前数组完全一致,否则抛TypeError; - 若
iterable不是 array,则它必须是可迭代的,且各元素类型可被追加进当前数组。
insert(index, value, /)
在位置 index 之前插入新元素 value;负数索引相对数组末尾定位,例如 a.insert(-1, x) 插入到最后一个元素之前。越界索引会被钳制到数组首尾。
pop(index=-1, /) 与 remove(value, /)
pop删除并返回索引index处的元素,默认-1即弹出末尾元素;remove删除第一个等于value的元素;未找到时抛ValueError。
count(value, /)
返回 value 在数组中出现的次数。
index(value[, start[, stop]])
返回 value 第一次出现的下标,找不到抛 ValueError;Python 3.10 起(versionchanged:: 3.10)支持可选参数 start 与 stop,只在闭区间内查找。
reverse() 与 clear()
reverse():就地反转元素顺序;clear():清空全部元素。Python 3.13 新增(versionadded:: 3.13),此前可通过del a[:]实现同样效果。
fromlist(list, /)
从列表逐项追加元素。它等价于 for x in list: a.append(x),但有一个重要的原子性差异:若中途发生类型错误,整个数组保持不变(即失败不产生部分写入)。其"失败即回滚"语义在 Lib/test/test_array.py 的 test_fromlist_reentrant_index_mutation 中也有针对畸形元素的重入测试。
六、文件与二进制字节读写方法
这一组方法让 array 成为读写二进制数据文件的利器——它按"机器值"而不是按 Python 对象序列化,因此读写两端(或 C 扩展侧)需要约定字节序与类型码。
frombytes(buffer, /)
把 bytes-like 对象的内容解释为一串机器值并追加到数组末尾(如同用 fromfile 从文件读出那样)。该方法在 Python 3.2 由 fromstring 更名而来(versionadded:: 3.2),以提高语义清晰度。
tobytes()
反向操作:把数组转成一串机器值字节序列并返回,其内容与用 tofile 写入文件得到的字节序列完全一致。同样在 Python 3.2 由 tostring 更名。
fromfile(f, n, /)
从文件对象 f 读取 n 个机器值并追加到数组。若文件不足 n 项,抛 EOFError——但已读到的部分仍然会被插入数组(不是整体回滚),使用时要留意这一半途状态。
tofile(f, /)
把所有元素以机器值形式写入文件对象 f。
一个把 Python 浮点数组与二进制文件互通的完整示例:
import array
# 写:100 万个双精度浮点 → 约 8MB 的原始二进制
values = array.array('d', [i ** 0.5 for i in range(1_000_000)])
with open('sqrt.dat', 'wb') as f:
values.tofile(f) # 字节数 == len(values) * values.itemsize
# 读:按同样的类型码还原
loaded = array.array('d')
with open('sqrt.dat', 'rb') as f:
loaded.fromfile(f, 1_000_000)
# 与单字节数组互相转换的场景
raw = array.array('B', values.tobytes())
fromunicode(ustr, /) 与 tounicode()
fromunicode(ustr):把 Unicode 字符串数据扩展到数组中,仅限类型码'w'的数组,否则抛ValueError;tounicode():把数组转换回 Unicode 字符串,同样只允许'w'。
若想在其他类型码数组中操作 Unicode 数据,文档给出两个等价桥接写法:追加数据用 array.frombytes(unicodestring.encode(enc));取回字符串用 array.tobytes().decode(enc)。
tolist()
把数组转换为包含相同元素的普通 list,用于摆脱类型约束、与普通 Python 逻辑互操作。内部实现上,array_repr 在非 'w' 类型时正是先调用 tolist() 再用 %R 格式化(见 Modules/arraymodule.c)。
七、字符串表示与 eval 往返保证
Array 对象的字符串表示具有固定形态 array(typecode, initializer),并满足一个特殊保证(Doc/library/array.rst):
- 数组为空时省略
initializer,如array('l'); - 否则:
'w'类型显示为 Unicode 字符串字面量,其它类型显示为数字列表; - 只要满足
from array import array,该字符串就能被eval还原成类型与值完全一致的数组; - 若数组中含浮点特殊值
inf/nan,则eval环境中还必须预先定义同名变量(这也解释了为什么官方示例直接写-inf, nan)。
from array import array
a = array('d', [1.0, 2.0, 3.14])
repr(a) # "array('d', [1.0, 2.0, 3.14])"
b = eval(repr(a)) # 得到类型与值均相同的数组
b == a # True
八、byteswap():跨字节序数据交换的利器
byteswap() 对数组中所有元素就地做字节序交换(Doc/library/array.rst)。要点:
- 仅支持元素尺寸为 1、2、4、8 或 16 字节的类型,其余类型(如
'b'、'B')抛RuntimeError; - 典型场景:读取在一台字节序不同的机器上写出的二进制文件后,调用一次
byteswap()即可把数据解释为本机字节序; - 对于复数类型(
'Zf'/'Zd',尺寸恰为 8/16),交换后分量顺序(先实部后虚部)保持不变——因为交换发生在每个分量内部而非整个复数块。
import array
# 模拟:从大端机器写出的 32 位整数文件读入小端机
buf = open('bigendian_ints.dat', 'rb').read()
a = array.array('i')
a.frombytes(buf) # 此时每个 int 的字节序是反的
a.byteswap() # 就地翻转为本机字节序
配合上一节的 frombytes/tobytes,这套组合足以完成任意二进制数值流在不同字节序主机间的迁移。
九、内存地址、buffer 接口与 C 层互操作
9.1 buffer_info():只读的内存地址查询(遗留 API)
buffer_info() 返回二元组 (address, length):address 是当前保存元素内容的内存缓冲区的起始地址,length 是元素个数而非字节数(Doc/library/array.rst)。缓冲总字节数可按下式计算:
total_bytes = a.buffer_info()[1] * a.itemsize
该数值只有在数组存活且未发生改变长度的操作时才有效。文档给出了两条重要告诫:
- 返回值适用于需要内存地址的低层(且固有地不安全)I/O 接口,例如部分
ioctl调用; - 若在 C/C++ 代码里使用 array(这才是该信息真正有意义的场合),更合理的方式是使用 buffer 协议而不是这个方法;
buffer_info()仅为向后兼容而保留,新代码应避免使用。buffer 协议的完整说明见 Doc/c-api/buffer.rst。
9.2 数组实现 buffer 接口
文档同时明确:array 对象实现了 buffer 协议,可以用于任何接受 bytes-like 对象的地方(Doc/library/array.rst)。C 结构体中的 ob_exports 字段正是用于跟踪"已导出的内存视图数",以保护底层连续缓冲区在其上存在视图时不被非法改动。这意味着:
import array
a = array.array('d', [1.5, 2.5, 3.5])
m = memoryview(a) # 对底层连续内存建立零拷贝视图
m.nbytes # == a.buffer_info()[1] * a.itemsize
memoryview 支持与数组共享内存而无需复制,是把 Python 数值数组交给 struct、mmap 乃至第三方 C 扩展处理的便捷通道。底层 array 的存储内存是一段连续分配的 nbytes = size * descr->itemsize 空间,分配前在 Modules/arraymodule.c 还会做 PY_SSIZE_T_MAX / itemsize 的溢出检查,避免超大尺寸请求导致整数溢出。
十、与 struct / ctypes / NumPy 的定位辨析
文档在"参见"中给出了两个标准库对照与一个第三方参照:
struct模块(Doc/library/struct.rst):用于异构二进制数据的打包与解包。array的frombytes/tobytes处理的是"同构、固定宽度"的机器值流;而struct允许在一行格式串里描述不同宽度的字段。两者常组合使用:struct解析记录头,array批量灌入数值区。ctypes模块:ctypes的 fundamental data types 也定义了一套类型码(如c_int、c_double),用于描述 C 数据结构的布局。- NumPy:定义了另一套 ndarray 类型,提供远超
array的向量化运算与广播能力。
关键提醒依然适用:三家类型码相似但彼此略有差异,混用时需显式映射。如果需要的是"紧凑存储 + 原生序列语义 + 与二进制/C 层互通",array 是零依赖的轻量选择;若要做科学计算,则应引入专门库。
十一、实现层面速览与深入阅读指引
把文档 API 与源码对照,可以帮助理解行为背后的设计取舍:
- 描述符表定义在 Modules/arraymodule.c:每个类型码对应一个
arraydescr,包含typecode、itemsize、取/赋值函数指针(*_getitem/*_setitem)、比较函数指针、is_integer_type与is_signed标志。新增类型码(如'e'、'Zf'/'Zd')时需要在描述符表与typecode_to_mformat_code()中同步维护。 - 导出方法表定义在 Modules/arraymodule.c:逐一对应文档列出的
append、buffer_info、byteswap、clear、count、extend、fromfile、fromlist、frombytes、fromunicode、index、insert、pop、remove、reverse、tofile、tolist、tobytes、tounicode,另有__copy__/__deepcopy__、__reduce_ex__(支持 pickle 序列化)以及__class_getitem__(泛型别名)。 - 机器格式编号:为了在不依赖编译器特定内存表示的前提下描述数组内容(尤其用于 pickle 与跨解释器还原),源码维护了
enum machine_format_code(Modules/arraymodule.c),把各类型码映射为"有符号/无符号整数、IEEE 浮点、UTF-16/UTF-32 及复数、半精度浮点"的大小端组合;测试侧在 Lib/test/test_array.py 中同步维护了这些编号的权威取值。 - 类型码集合的构建:模块初始化时先收集描述符表中的类型码构成列表,再转换为元组存入
typecodes(见 Modules/arraymodule.c),这正是"3.15 起typecodes从字符串变为元组"这一行为的来源。
版本背景:本文所述内容基于当前仓库(CPython 开发版,见 Include/patchlevel.h),其中
'w'、clear()属 Python 3.13 新增能力,'e'、'Zf'/'Zd'与typecodes变更为元组属 Python 3.15 新增/变更能力。若你的运行版本更低,请以实际环境help(array)或官方对应版本的 Doc/library/array.rst 为准。
小结
array.array 是 CPython 标准库中"以 list 的编程体验、以类 C 数组的内存效率"存取同质数值数据的方案:用一张类型码表锁定元素类型,通过统一的可变序列接口操作元素,通过 frombytes/tobytes/fromfile/tofile/byteswap 完成与二进制数据流、跨字节序文件及 C 扩展(buffer 协议)的互操作,并通过 itemsize/buffer_info/memoryview 精确掌控内存布局。当你的数据规模大到让 Python 对象的盒子开销成为瓶颈、但又不至于需要 NumPy 的全套向量化能力时,它就是最贴合标准库语义的选择。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00