首页
/ CPython array 模块深入解析:类型化数值数组的高效内存表示与完整 API 指南

CPython array 模块深入解析:类型化数值数组的高效内存表示与完整 API 指南

2026-09-06 18:35:10作者:董宙帆

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.MutableSequenceReversible,即它完整地实现了标准可变序列协议。

由于浮点与整数在 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)

三个备注的含义需要特别说明:

  1. 'w'(Unicode 字符):Python 3.13 新增(versionadded:: 3.13)。其 C 类型为 Py_UCS4,即一个码点占 4 字节,与 list[str] 相比内存更紧凑。
  2. '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} 定义——注意其 itemsizesizeof(short)(2 字节),且没有 compare 回调。
  3. 'Zf' / 'Zd'(复数):Python 3.15 新增,且文档明确它们是无条件可用的,不依赖 C 编译器对 C11 Annex G(复数)的支持。按 C11 规定,每个复数类型由"实部、虚部"两元素的 C 数组表示,因此描述符中 Zf2*sizeof(float)Zd2*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 之前它是 strversionchanged:: 3.15 记录其从 str 变为 tuple)。测试 Lib/test/test_array.py 对每个类型码逐一验证了它们是 tuple 中长度 ≥ 1 的 str 元素。

类型码的既有生态提醒:ctypesstruct 以及第三方库(如 NumPy)使用相似但不完全相同的类型码体系,跨库转换时务必核对各家格式字符。相关背景可对照 Doc/library/struct.rstDoc/library/ctypes.rst


三、构造与初始化:typecode 必选、initializer 可选

class array(typecode[, initializer])

typecode 是唯一必选参数;可选的 initializer 必须满足以下三类情形之一,否则抛 TypeError

  1. bytesbytearray 对象:整个 initializer 会被传给新数组的 frombytes() 方法(按机器值逐字节解释,见下文)。
  2. Unicode 字符串:会被传给 fromunicode() 方法——只对 'w' 数组合法,否则抛 ValueError
  3. 其它一切可迭代对象:其迭代器被传给 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])

注意最后一行浮点数组里直接使用了 -infnan 这样的名字——这并非语法糖,而是与下一节"字符串表示可被 eval 还原"的保证相关。

3.1 支持可变序列的全部常规操作

Array 对象支持 typing 层面标注的可变序列常规操作:索引、切片、拼接与乘法(Doc/library/array.rst)。但有两条类型约束必须牢记:

  • 切片赋值时,右侧必须是同类型码的 array 对象,其它类型一律抛 TypeError
  • 元素赋值、extend 等操作要求元素类型可被转换为数组的类型码。

空数组的边界行为(切片自赋值、+*)在 Lib/test/test_array.pytest_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)支持可选参数 startstop,只在闭区间内查找。

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.pytest_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 数值数组交给 structmmap 乃至第三方 C 扩展处理的便捷通道。底层 array 的存储内存是一段连续分配的 nbytes = size * descr->itemsize 空间,分配前在 Modules/arraymodule.c 还会做 PY_SSIZE_T_MAX / itemsize 的溢出检查,避免超大尺寸请求导致整数溢出。


十、与 struct / ctypes / NumPy 的定位辨析

文档在"参见"中给出了两个标准库对照与一个第三方参照:

  • struct 模块Doc/library/struct.rst):用于异构二进制数据的打包与解包。arrayfrombytes/tobytes 处理的是"同构、固定宽度"的机器值流;而 struct 允许在一行格式串里描述不同宽度的字段。两者常组合使用:struct 解析记录头,array 批量灌入数值区。
  • ctypes 模块ctypes 的 fundamental data types 也定义了一套类型码(如 c_intc_double),用于描述 C 数据结构的布局。
  • NumPy:定义了另一套 ndarray 类型,提供远超 array 的向量化运算与广播能力。

关键提醒依然适用:三家类型码相似但彼此略有差异,混用时需显式映射。如果需要的是"紧凑存储 + 原生序列语义 + 与二进制/C 层互通",array 是零依赖的轻量选择;若要做科学计算,则应引入专门库。


十一、实现层面速览与深入阅读指引

把文档 API 与源码对照,可以帮助理解行为背后的设计取舍:

  • 描述符表定义在 Modules/arraymodule.c:每个类型码对应一个 arraydescr,包含 typecodeitemsize、取/赋值函数指针(*_getitem/*_setitem)、比较函数指针、is_integer_typeis_signed 标志。新增类型码(如 'e''Zf'/'Zd')时需要在描述符表与 typecode_to_mformat_code() 中同步维护。
  • 导出方法表定义在 Modules/arraymodule.c:逐一对应文档列出的 appendbuffer_infobyteswapclearcountextendfromfilefromlistfrombytesfromunicodeindexinsertpopremovereversetofiletolisttobytestounicode,另有 __copy__/__deepcopy____reduce_ex__(支持 pickle 序列化)以及 __class_getitem__(泛型别名)。
  • 机器格式编号:为了在不依赖编译器特定内存表示的前提下描述数组内容(尤其用于 pickle 与跨解释器还原),源码维护了 enum machine_format_codeModules/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 的全套向量化能力时,它就是最贴合标准库语义的选择。

登录后查看全文
热门项目推荐
相关项目推荐