Python数组定义全解析:list、array与numpy选型指南

发布时间:2026/11/3 23:55:26
Python数组定义全解析:list、array与numpy选型指南 很多刚接触 Python 的人第一次听到“数组”这个词脑子里浮现的往往是 C 语言里那种int arr[10]的固定长度结构结果一上手就发现 Python 里根本没有这种原生的东西于是开始犯迷糊到底该用list、array、还是numpy.ndarray我见过太多人在项目里随手用列表当数组使数据量一上来内存直接翻倍或者在做数值计算时被 Python 原生循环拖到怀疑人生。这篇内容就是想把“Python 里数组到底怎么定义、怎么选、怎么用”这件事彻底讲透从最基础的字面量定义到array模块的类型码再到numpy的向量化操作配上我实际踩过的坑和性能对比数据让不管是刚入门还是已经写了一阵子的人都能找到适合自己场景的那套方案。1. 先把“数组”这个词在 Python 里拆开看1.1 为什么 Python 没有 C 那种原生数组Python 的设计哲学里一切皆对象变量只是贴在对象上的标签。C 语言里的数组是一块连续内存变量名直接指向首地址长度固定、类型固定编译器在栈上就能把空间算好。Python 的动态类型和对象模型决定了它没法在语言层面提供这种结构因为每个元素都得是一个完整的 PyObject带引用计数、带类型指针。所以 Python 把“连续同类型数据”这件事交给了标准库和第三方库去解决语言本身只保留了一个最通用的容器——list。这个设计取舍带来的直接后果是list里存的其实是指针不是值本身。一个存 100 万个整数的列表实际占用内存远不止 100 万个整数该占的 8MB而是每个元素一个 PyObject 指针加对象头轻松飙到 30MB 以上。这就是为什么你在处理大规模数值时必须换用别的结构而不是硬拿列表凑合。1.2 list、array.array、numpy.ndarray 三者的定位差异我把这三者的核心区别整理成一张表先建立整体认知后面再逐个展开特性listarray.arraynumpy.ndarray元素类型任意对象单一基础类型单一 dtype内存布局指针数组连续值存储连续值存储是否支持多维嵌套实现仅一维原生多维数值运算需循环需循环向量化标准库自带是是否需安装典型场景通用容器省内存的一维数值科学计算、矩阵选型的判断逻辑其实很简单如果你只是存一堆东西、增删频繁、类型混杂用list如果你要存大量同类型的一维数值、又不想引入第三方依赖用array.array如果你要做矩阵运算、切片统计、广播计算直接上numpy别犹豫。我见过有人用嵌套列表手写矩阵乘法三层循环跑了几分钟换成numpy一行运算零点几秒搞定这种差距不是优化技巧能弥补的是结构选错了。1.3 一个容易混淆的点列表推导式不是数组定义新手常把[x for x in range(10)]当成“定义数组”严格说它定义的是列表。语法上没错但概念上要清楚你得到的是一个list对象不是连续内存的数组。这个区分在数据量小的时候无所谓一旦上了规模内存和速度的差异就会暴露出来。我在一个日志分析的小工具里做过对比同样存 500 万个浮点数列表占约 200MBarray.array(d)只占约 40MB差了五倍。所以定义之前先想清楚数据规模和用途比事后优化省事得多。2. 用 list 定义“数组”最通用但也最容易误用2.1 五种常见的列表定义写法及其适用场合列表的定义方式很多不同写法背后其实对应不同的使用意图我把常用的几种列出来# 方式一字面量元素已知且固定 nums [1, 2, 3, 4, 5] # 方式二乘法初始化适合占位 zeros [0] * 100 # 方式三列表推导式适合按规则生成 squares [i * i for i in range(10)] # 方式四从可迭代对象转换 from_range list(range(10)) # 方式五空列表逐步追加 result [] for i in range(10): result.append(i)这里有个经典陷阱必须点出来[[0] * 3] * 3这种写法看起来像是定义了一个 3x3 的二维数组实际上三行指向的是同一个内层列表对象。你改matrix[0][0] 1三行的第一个元素全变了。正确写法是[[0] * 3 for _ in range(3)]用推导式让每一行都是独立对象。这个坑我当年调试了半小时才反应过来因为打印出来结构看着完全正常只有修改时才暴露。2.2 预分配与 append 的性能取舍很多人纠结到底该先[None] * n预分配再按下标赋值还是直接append。实测结论是对于纯追加场景append的摊还成本很低CPython 的列表会按约 1.125 倍超额分配容量均摊下来每次追加接近 O(1)没必要手动预分配。但如果你需要按下标随机写入、且事先知道长度预分配能避免多次扩容时的内存拷贝尤其在百万级数据上差异明显。我做过一个简单测试往列表里塞 1000 万个整数import time # 方式一append start time.perf_counter() a [] for i in range(10_000_000): a.append(i) print(append:, time.perf_counter() - start) # 方式二预分配 start time.perf_counter() b [0] * 10_000_000 for i in range(10_000_000): b[i] i print(preallocate:, time.perf_counter() - start)在我的机器上预分配版本通常快 20% 到 30%因为省掉了扩容判断和偶发的整块拷贝。但注意这个优势只在“已知长度且按下标写”时成立如果长度不确定预分配反而浪费。2.3 列表当数组用的边界在哪里列表能当数组用但有几个边界要心里有数。第一元素类型不统一时内存开销大一个列表里混着整数、字符串、None每个都是独立对象。第二数值运算必须走 Python 层循环没法利用底层 SIMD 指令。第三多维结构靠嵌套实现访问matrix[i][j]要两次索引缓存局部性差。当你的数据满足“同类型、一维或规则多维、需要大量数值计算”这三个条件中的两个以上时就该考虑换结构了。反过来如果只是配置项、混合类型记录、频繁增删的队列列表就是最合适的选择别为了“看起来像数组”而过度设计。3. array 模块标准库里的省内存方案3.1 类型码怎么选选错了会怎样array.array的核心是类型码它决定了每个元素占几个字节、怎么解释二进制。常用的类型码有这么几个类型码C 类型字节数取值范围bsigned char1-128 ~ 127Bunsigned char10 ~ 255hsigned short2-32768 ~ 32767isigned int2 或 4依平台lsigned long4 或 8依平台ffloat4单精度ddouble8双精度选类型码最容易犯的错是拿i存超出范围的值。比如你想存一个 40 亿的计数i在多数平台是 4 字节有符号上限约 21 亿赋值时直接抛OverflowError。这种错误在测试数据小时发现不了上线跑大流量才炸。我的习惯是明确知道数据范围就用最小够用的类型码省内存范围不确定就用l或q8 字节有符号别为了省几个字节埋雷。import array # 定义并初始化 a array.array(i, [1, 2, 3, 4, 5]) print(a.typecode, a.itemsize) # i 4 # 从字节流构造注意字节序 raw a.tobytes() b array.array(i) b.frombytes(raw) print(b) # array(i, [1, 2, 3, 4, 5])3.2 和列表互转时的隐性成本array.array和list互转很方便array.array(i, some_list)和list(some_array)都行但要知道这中间发生了什么。转成 array 时每个 Python 整数对象要被拆箱成 C 值写入连续内存转回列表时每个 C 值要重新装箱成 PyObject。这两个方向都是 O(n) 且带对象分配开销百万级数据转换一次几十毫秒到上百毫秒不等。所以别在热循环里反复互转比如“array 转 list 处理一下再转回 array”这种写法性能直接崩掉。要么全程用 array 配合它的方法要么一开始就用列表别来回折腾。3.3 什么场景下 array 比 numpy 更合适有人觉得有了 numpy 就不需要 array 了其实不然。array 是标准库零依赖在不能随便装包的受限环境里比如某些嵌入式或精简镜像它是唯一选择。另外当你只需要一个省内存的一维数值容器、不做任何向量运算时array 的启动开销比 numpy 小得多。numpy 导入本身就要几十毫秒创建一个 ndarray 也有额外元数据开销而 array 轻量直接。我有个小脚本只是把一批二进制数据读进来做简单遍历统计用 array 比 numpy 快且省事因为根本用不上向量化。判断标准就一条需要向量化运算和广播就上 numpy只是存和取就用 array。4. numpy 数组数值计算的正解4.1 从列表创建 ndarray 的几种路径与 dtype 推断numpy 数组的定义入口很多最常用的是np.array()但它的 dtype 推断规则值得说清楚import numpy as np # 整数列表默认推断为 int6464 位平台 a np.array([1, 2, 3]) print(a.dtype) # int64 # 混合整数和浮点向上提升为 float64 b np.array([1, 2.5, 3]) print(b.dtype) # float64 # 显式指定 dtype避免推断带来的意外 c np.array([1, 2, 3], dtypenp.float32) print(c.dtype) # float32这里有个实际项目里的教训默认推断成int64或float64看着省心但内存是int32/float32的两倍。做图像处理或大规模特征矩阵时一个 10000x10000 的 float64 矩阵占 800MB换成 float32 直接减半到 400MB而且多数场景精度完全够用。所以我的习惯是创建时显式写 dtype尤其是数据量大的时候别让默认推断替你做决定。4.2 zeros、ones、arange、linspace 的适用边界除了从现有数据转换numpy 还提供了一批生成函数各自适用场景不同np.zeros(shape)全零初始化适合先占位再填充比如累积求和的缓冲区。np.ones(shape)全一常用于掩码或权重初始化。np.arange(start, stop, step)按步长生成注意浮点步长会有精度累积误差。np.linspace(start, stop, num)按数量等分适合画图坐标轴、采样点。arange用浮点步长是个隐蔽的坑。比如np.arange(0, 1, 0.1)得到的最后一个值可能不是 0.9 而是 0.8999999999999999元素个数也可能差一个。需要精确等分时一律用linspace它内部按(stop-start)/(num-1)计算端点精确可控。这个细节在做信号采样或绘图刻度时特别重要差一个点整条曲线就对不齐。4.3 向量化为什么快从字节码到 SIMDnumpy 快的根本原因不是“用 C 写的”这么简单而是它把循环下沉到了编译好的底层代码里并且数据在连续内存上排布能利用 CPU 的 SIMD 指令一次处理多个元素。对比一下import numpy as np import time n 10_000_000 lst list(range(n)) arr np.arange(n) # 列表Python 层循环 start time.perf_counter() s1 sum(x * 2 for x in lst) print(list:, time.perf_counter() - start) # numpy向量化 start time.perf_counter() s2 (arr * 2).sum() print(numpy:, time.perf_counter() - start)实测差距通常在 20 倍以上数据越大差距越明显。关键在于 numpy 的arr * 2不产生 Python 层的逐元素循环而是在 C 层遍历连续内存配合 SIMD 一条指令处理 4 个或 8 个 float。这就是为什么“能用向量化就别写 for 循环”在 numpy 里是铁律不是风格问题而是性能问题。5. 定义之后索引、切片与形状操作的实操细节5.1 切片是视图还是拷贝这个区别能坑死人列表切片返回新列表拷贝numpy 切片返回视图共享内存这个差异是新手翻车重灾区import numpy as np arr np.arange(10) sub arr[2:5] sub[0] 999 print(arr) # [0 1 999 3 4 5 6 7 8 9]原数组被改了 # 想要独立副本显式 copy sub2 arr[2:5].copy() sub2[0] 0 print(arr) # 原数组不受影响我见过有人在函数里对传入的 numpy 数组切片后原地修改以为改的是局部副本结果把调用方的数据污染了排查半天才发现是视图共享。记住一条numpy 的基础切片是视图花式索引用列表或布尔数组做下标才是拷贝。不确定的时候加.copy()多一次内存拷贝总比数据被意外改掉强。5.2 reshape 的 -1 参数与内存连续性reshape里用-1让 numpy 自动推算维度非常方便比如arr.reshape(-1, 3)表示“列数固定 3行数你算”。但要注意 reshape 默认按 C 顺序行优先读取如果原数组不是连续内存比如经过转置reshape 可能触发一次拷贝。判断方法是用arr.flags[C_CONTIGUOUS]查看。对性能敏感的场景转置后立刻 reshape 要留意这个隐性拷贝必要时用np.ascontiguousarray()显式整理内存布局。5.3 布尔索引与花式索引的返回类型差异布尔索引arr[arr 5]返回的是一维拷贝花式索引arr[[0, 2, 4]]也是拷贝两者都不共享内存。这个特性在筛选数据时很好用但如果你以为筛选后还能反向影响原数组就错了。另外布尔索引要求布尔数组形状和原数组匹配形状对不上会直接报错这点比列表的松散行为严格写的时候要保证条件表达式作用在正确的轴上。6. 踩过的坑与性能实测记录6.1 一个真实的内存翻倍事故早些年做一个数据清洗脚本读进来一批传感器数据每行是时间戳加若干浮点读数。我图省事用嵌套列表存外层列表每行一个内层列表。数据量到 200 万行、每行 20 个浮点数时进程内存直接冲到 3GB 多机器开始换页。后来改成np.array(rows, dtypenp.float32)同样的数据只占约 160MB降了将近 20 倍。根因就是列表存的是指针加独立 PyObject每个浮点数都有对象头开销而 ndarray 是紧凑的连续 float32。这个教训让我后来凡是数值型批量数据第一反应就是 numpy不再用列表硬扛。6.2 类型码选错导致的溢出排查过程有次写一个统计二进制协议字段的脚本用array.array(h)存某个计数字段测试数据都是小值没问题。上线后遇到大流量时段计数超过 32767脚本直接抛OverflowError崩了。排查时先看报错行是append调用再看类型码h是 2 字节有符号上限 32767立刻明白。改成l后问题消失。这个坑的教训是类型码选择要按字段的理论最大值来定而不是按测试数据的范围协议字段尤其要看规范里的位宽定义。6.3 三种结构在百万级数据上的实测对比我做过一组对照测试存 100 万个整数并求和结果大致如下具体数值因机器而异看相对关系结构内存占用求和耗时list约 36MB约 30msarray(i)约 4MB约 25msnumpy int32约 4MB约 1ms内存上 array 和 numpy 接近都是紧凑存储但求和耗时 numpy 快一个数量级因为它用了向量化的归约实现。列表的求和虽然也是 C 层sum但每个元素要拆箱开销大。这组数据说明省内存看 array 或 numpy要速度就 numpy列表在纯数值场景两头不占优。6.4 几个我反复提醒自己的注意事项第一定义数组前先问数据规模和类型规模小随便用列表规模大且同类型就换结构别等性能出问题再重构。第二numpy 切片默认是视图函数内修改入参前想清楚是不是要.copy()。第三array的类型码按理论范围选别按测试数据选。第四能用向量化表达的就别写 Python 循环这是 numpy 场景下性价比最高的优化。第五多维列表初始化别用乘法用推导式。这几条都是真金白银踩出来的写在这里给后来人省点调试时间。7. 按场景给出可直接抄的选型清单7.1 日常脚本与小数据列表足够如果你的数据量在几万以内、类型混杂、需要频繁增删直接用列表别引入任何额外结构。定义就用字面量或推导式读取用索引和切片简单直接。这个场景下追求“数组感”反而是过度设计维护成本比性能收益高。7.2 受限环境的一维数值array 模块不能装第三方包、又要存大量同类型一维数值时array.array是标准答案。定义时明确类型码读写用它的append、extend、frombytes等方法避免和列表来回转换。适合二进制数据解析、简单统计这类轻量任务。7.3 数值计算与矩阵numpy 一步到位涉及矩阵运算、批量统计、图像处理、机器学习特征时直接 numpy定义时显式指定 dtype 控制内存运算全程向量化切片注意视图语义。这是投入产出比最高的选择前期花点时间熟悉 API后面省下的是成倍的时间。7.4 一张决策流程图式的判断口诀我总结了一个顺口溜式的判断顺序先看要不要向量运算要就 numpy不要就看数据量大不大大且同类型就 array都不满足就用 list。按这个顺序走基本不会选错。真正难的不是记住这个顺序而是在写代码前停下来想三秒钟而不是条件反射地敲出[]。这三秒钟的思考往往决定了后面是顺利交付还是熬夜调性能。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询