Python数组选型指南:list、array与numpy性能对比与避坑

发布时间:2026/11/3 23:55:26
Python数组选型指南:list、array与numpy性能对比与避坑 很多刚接触 Python 的朋友第一次听到“数组”这个词脑子里浮现的往往是 C 语言里那种定长、连续内存、类型固定的结构结果一上手 Python 就懵了一会儿是list一会儿是array还有numpy.array、tuple、bytearray到底哪个才算“数组”我在带新人的时候被问过无数次这个问题也见过不少人因为选错了容器程序跑得慢得离谱或者莫名其妙踩了类型混用的坑。这篇内容就把 Python 里“数组”这件事从头到尾捋一遍从最常用的列表讲起到标准库的array模块再到数值计算场景下的numpy数组把定义方式、使用技巧、性能差异和常见坑都摊开说清楚。不管你是刚学 Python 想搞明白基础容器还是已经写过一些代码但想弄清楚“什么时候该用哪个”的进阶读者看完都能直接拿去用。1. 先搞清楚 Python 里“数组”到底指什么1.1 为什么 Python 没有 C 那种原生数组Python 是一门动态类型语言变量本质上是对对象的引用容器里存的也是引用而不是值本身。这个设计决定了它没法像 C 那样在编译期就确定一块连续内存、规定每个元素占几个字节。所以严格意义上Python 内置类型里并没有 C 语言意义上的“数组”。我们平时口语说的“Python 数组”其实是一个泛指涵盖了列表、array模块、numpy数组等好几种东西。这个背景很重要因为它直接解释了后面所有的选型逻辑。列表灵活但每个元素都是独立对象有额外的对象头开销array模块牺牲了灵活性换来紧凑存储numpy数组则是在紧凑存储的基础上加上了向量化运算能力。你选哪个取决于你要的是“灵活”还是“省内存”还是“算得快”。我一般会跟新人这么打比方列表像是一个可以装任何东西的收纳箱每个格子都能塞不同形状的物品array像是一排规格统一的抽屉只能放同一种规格的东西但排列得特别紧凑numpy数组则像是一整块集成电路不光排列紧凑还能整块一起做运算。理解了这个类比后面的选择就不难了。1.2 四种常见“数组”容器速览在动手写代码之前先把这几个候选摆出来对比一下心里有个谱。容器类型是否内置元素类型要求内存效率运算能力典型场景list内置任意类型混用较低对象引用对象头需手写循环通用数据组织、增删频繁tuple内置任意类型混用较低需手写循环不可变数据、函数返回多值array.array标准库单一数值类型高连续存储需手写循环大量同类型数值、省内存numpy.ndarray第三方单一类型可多维高向量化运算科学计算、矩阵运算、数据分析这张表建议先记住结论日常写业务逻辑用list需要不可变用tuple纯粹存大量数值又想省内存用array要做数值计算用numpy。下面逐个展开。1.3 一个容易混淆的点列表不是数组很多人把list直接叫数组这在口语里没问题但在讨论性能和内存时一定要区分开。列表的每个元素都是一个 PyObject 指针指向堆上的对象。也就是说一个存了 100 万个整数的列表实际占用的内存远不止 100 万个整数本身还包括 100 万个指针每个 8 字节加上每个整数对象自身的开销小整数有缓存大整数则是独立对象。而array.array(i, ...)存 100 万个 32 位整数就是实打实的约 4MB 连续内存没有额外对象开销。这个差异在数据量大的时候非常明显我实测过一个存 500 万个整数的场景列表占用接近 200MB而array只用了约 20MB差了将近十倍。所以如果你的场景是“大量同类型数值”别犹豫往array或numpy走。2. 列表的定义与日常使用要点2.1 五种定义方式与各自适用场景列表的定义方式看着简单但不同写法背后其实有适用场景的差异用错了容易出问题。# 方式一字面量最常用 nums [1, 2, 3, 4, 5] # 方式二空列表后续追加 result [] result.append(10) # 方式三list() 构造函数从可迭代对象转换 chars list(hello) # [h, e, l, l, o] ranges list(range(5)) # [0, 1, 2, 3, 4] # 方式四列表推导式带条件生成 squares [x * x for x in range(10) if x % 2 0] # 方式五乘法快速初始化注意陷阱见 2.2 grid [0] * 10字面量方式最直观适合元素已知且数量不多的情况。list()构造函数适合从其他可迭代对象转换比如字符串、range、生成器。列表推导式是 Python 的招牌特性既简洁又通常比手写循环快因为推导式的循环在 C 层面执行。乘法初始化适合快速造一个固定长度的占位列表但这里有个大坑下一节专门说。我个人的习惯是能用推导式就用推导式可读性和性能都占优需要动态追加时先建空列表再append只有在明确知道所有元素时才用字面量。这个习惯坚持下来代码会干净很多。2.2 乘法初始化的经典陷阱[0] * 10这种写法对不可变元素数字、字符串、元组是安全的但对可变元素就是灾难。看这段代码# 错误示范二维列表不能用乘法初始化 bad [[0] * 3] * 3 bad[0][0] 1 print(bad) # 输出[[1, 0, 0], [1, 0, 0], [1, 0, 0]]你以为创建了三行独立的列表实际上三行指向的是同一个列表对象。改一个全变。这个坑我见过太多人踩尤其是写矩阵、棋盘、表格类逻辑的时候。正确做法是用推导式每轮循环都新建一个独立对象# 正确示范 good [[0] * 3 for _ in range(3)] good[0][0] 1 print(good) # 输出[[1, 0, 0], [0, 0, 0], [0, 0, 0]]注意判断一个初始化写法是否安全就看“被复制的元素是不是可变对象”。不可变对象随便乘可变对象必须用推导式逐个创建。2.3 增删改查的高频操作与性能直觉列表的常用操作不难但要知道哪些快哪些慢写代码时才有性能直觉。nums [3, 1, 4, 1, 5] nums.append(9) # 尾部追加O(1) 均摊 nums.insert(0, 0) # 头部插入O(n)要挪动所有元素 nums.pop() # 尾部弹出O(1) nums.pop(0) # 头部弹出O(n) nums.remove(1) # 按值删除第一个匹配项O(n) nums.index(4) # 查找索引O(n) nums[2] # 随机访问O(1)关键结论尾部操作快头部和中间操作慢。如果你需要频繁在两端操作用collections.deque而不是列表。如果频繁按值查找考虑换成set或dict。这些替换在数据量大时能带来数量级的提升。我踩过的一个真实坑早期写一个任务队列用列表的pop(0)取任务数据量小的时候没感觉任务堆到几万个之后明显卡顿。后来换成deque的popleft()问题立刻消失。所以别小看这些复杂度差异量变会引起质变。2.4 切片列表最被低估的能力切片是列表里性价比最高的特性之一用好了能省掉大量循环代码。data [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] data[2:5] # [2, 3, 4]左闭右开 data[:3] # [0, 1, 2]省略起点 data[7:] # [7, 8, 9]省略终点 data[::2] # [0, 2, 4, 6, 8]步长为 2 data[::-1] # 反转整个列表 data[8:2:-2] # [8, 6, 4]负步长倒着取切片返回的是新列表原列表不受影响这一点在需要“复制一份再改”的场景特别有用。但要注意浅拷贝的问题如果列表元素是可变对象切片出来的新列表里元素还是同一批对象引用改元素内部状态会互相影响。需要深拷贝时用copy.deepcopy()。另一个实用技巧是用切片做原地替换比如data[1:3] [100, 200, 300]这会把索引 1 到 2 的元素替换成三个新元素列表长度随之变化。这个操作在批量修改时很顺手但长度变化容易算错建议改完立刻打印确认。3. array 模块省内存的紧凑数组3.1 什么时候该从列表切换到 array列表用着舒服但内存开销大。当你满足下面几个条件时就该考虑array模块了元素全是同一种数值类型整数或浮点数、数据量比较大几万以上、不需要存储字符串或自定义对象、对内存敏感。array的核心价值就是紧凑。它用类型码typecode声明元素类型底层是连续的 C 数组没有 Python 对象头开销。常用类型码如下类型码C 类型字节数取值范围/说明bsigned char1-128 到 127Bunsigned char10 到 255hsigned short2-32768 到 32767isigned int2 或 4平台相关通常 4 字节lsigned long4 或 8平台相关ffloat4单精度浮点ddouble8双精度浮点选类型码的原则是“够用就好”。比如存 0 到 255 的像素值用B就比i省四分之三内存。存金额这种需要精度的别用浮点类型码老老实实用整数存“分”或者用decimal。3.2 定义与基本操作实操from array import array # 定义类型码 初始数据 a array(i, [1, 2, 3, 4, 5]) b array(d, [1.5, 2.5, 3.5]) c array(B, [255, 0, 128]) # 从字节流构造处理二进制数据时常用 raw bytes([1, 2, 3, 4]) d array(B) d.frombytes(raw) # 基本操作和列表类似 a.append(6) a.extend([7, 8]) print(a[0], a[-1]) print(len(a)) # 转回列表 as_list a.tolist() # 转成字节流写文件、网络传输时用 as_bytes a.tobytes()array的接口刻意做得和列表很像所以从列表迁移过来成本很低。但有两个限制要记住一是不能存混合类型array(i, [1, a])会直接抛TypeError二是不能存None需要空值时得用哨兵值比如 -1 或特定大数代替。3.3 类型码选错会怎样一个真实排查案例类型码选错是array使用中最隐蔽的问题因为它在赋值时才报错而且报错信息不一定直观。from array import array a array(b, [100, 120]) try: a.append(200) # b 最大 127溢出 except OverflowError as e: print(f溢出错误{e})上面这段会抛OverflowError。更麻烦的是浮点类型码存整数时的精度问题array(f, [0.1])存的是单精度取出来可能是0.10000000149011612。如果你的业务对精度敏感比如金额、坐标一定要用d双精度或者干脆别用array。我遇到过一个案例某同学用f存传感器读数累加几千次之后误差肉眼可见排查了半天以为是算法问题最后发现是单精度浮点的累积误差。换成d之后立刻正常。所以类型码这件事宁可多花一分钟确认也别事后返工。3.4 array 与列表互转的性能考量tolist()和array(typecode, list)这两个转换操作都是 O(n)会创建新对象。频繁互转会让array省内存的优势荡然无存因为转换过程中两份数据同时存在。正确的用法是在数据入口处一次性转成array中间处理全程用array出口处再转回需要的格式。别在循环里反复转。如果确实需要频繁在两种表示间切换说明你的场景可能不适合array老老实实用列表或者上numpy。4. numpy 数组数值计算的主力4.1 从列表到 ndarray 的思维转变numpy数组和列表最大的区别不是存储方式而是运算模型。列表做加法是拼接numpy数组做加法是逐元素相加。这个差异决定了写法的根本不同。import numpy as np # 列表加法是拼接 lst [1, 2, 3] [4, 5, 6] # [1, 2, 3, 4, 5, 6] # numpy加法是逐元素 arr np.array([1, 2, 3]) np.array([4, 5, 6]) # [5, 7, 9] # 标量广播 arr2 np.array([1, 2, 3]) * 10 # [10, 20, 30]这个“向量化”思维是用好numpy的关键。一旦你习惯了“整个数组一起算”就会发现以前写的很多 for 循环都是多余的而且慢。numpy的运算在 C 层面执行比 Python 循环快一到两个数量级是常态。4.2 创建数组的常用方法import numpy as np # 从列表创建 a np.array([1, 2, 3, 4]) # 指定数据类型 b np.array([1, 2, 3], dtypenp.float64) # 全零、全一 c np.zeros((3, 4)) # 3 行 4 列浮点 d np.ones((2, 3), dtypeint) # 等差序列 e np.arange(0, 10, 2) # [0, 2, 4, 6, 8] f np.linspace(0, 1, 5) # [0, 0.25, 0.5, 0.75, 1.0]含终点 # 随机数组 g np.random.rand(3, 3) # 0 到 1 均匀分布 h np.random.randn(100) # 标准正态分布 # 单位矩阵 i np.eye(3)arange和linspace的区别值得单独说arange按步长生成不含终点除非步长算得刚好linspace按数量生成默认含终点。做绘图、采样时用linspace更省心因为你能精确控制点数。4.3 形状操作reshape、转置与广播形状操作是numpy的高频需求也是新手最容易晕的地方。a np.arange(12) # [0, 1, ..., 11] b a.reshape(3, 4) # 3 行 4 列 c a.reshape(4, -1) # -1 表示自动推算这里是 3 列 d b.T # 转置4 行 3 列 e b.flatten() # 拉平成一维返回副本 f b.ravel() # 拉平尽量返回视图reshape要求元素总数匹配否则报错。-1是个好用的占位符让numpy自己算那一维。flatten和ravel的区别在于是否复制flatten一定复制ravel尽量返回视图。改视图会影响原数组这个细节在调试时经常让人困惑建议不确定时用flatten求稳。广播机制是numpy的精髓简单说就是“形状不同的数组在运算时自动对齐”。规则是从尾部维度开始比较要么相等要么其中一个是 1要么其中一个缺失。比如(3, 4)和(4,)可以相加后者被广播成(3, 4)。理解广播能让你少写很多显式循环但也要小心意外广播导致的形状膨胀算完记得检查结果形状。4.4 索引与切片的进阶用法numpy的索引比列表强大得多布尔索引和花式索引是两个杀手锏。a np.array([10, 20, 30, 40, 50]) # 布尔索引按条件筛选 mask a 25 print(a[mask]) # [30, 40, 50] # 花式索引按索引数组取值 idx [0, 2, 4] print(a[idx]) # [10, 30, 50] # 二维数组的行列切片 b np.arange(12).reshape(3, 4) print(b[1, :]) # 第二行 print(b[:, 2]) # 第三列 print(b[0:2, 1:3]) # 子矩阵 # 条件赋值 a[a 25] 0 print(a) # [10, 20, 0, 0, 0]布尔索引在数据清洗里用得极多比如“把异常值置零”“筛选满足条件的样本”。花式索引适合按任意顺序取子集。这两个特性配合起来很多原本要写循环的逻辑都能一行搞定。注意numpy的切片返回的是视图而不是副本改切片会影响原数组。需要独立副本时用.copy()。这个和列表的行为不同从列表转过来的人特别容易在这里翻车。5. 选型决策与性能实测对比5.1 一张决策流程图文字版选哪个容器按下面的顺序问自己元素类型是否统一且是数值否用list。是否需要做数学运算加减乘除、矩阵、统计是用numpy。数据量是否很大且内存敏感是用array。是否需要频繁增删是用list或deque。数据是否不可变是用tuple。大部分业务代码停在第一步就够了list能解决八成问题。真正需要array或numpy的场景往往是数据量上来了、或者要做数值计算了这时候再切换也不迟。别为了“显得专业”一上来就numpy小数据量下numpy的创建开销反而更大。5.2 内存与速度的实测数据我在一台普通开发机上做过一组对比存 100 万个整数结果如下数值为大致量级不同环境会有浮动操作listarray(i)numpy int64内存占用约 36MB约 4MB约 8MB创建耗时约 60ms约 25ms约 5ms全量求和约 15ms约 12ms约 1ms逐元素乘 2约 80ms约 70ms约 2ms结论很清晰内存上array最省速度上numpy碾压。列表在两者之间都不占优但胜在灵活。这组数据也解释了为什么数据处理场景几乎清一色用numpy——向量化运算带来的差距是数量级的不是靠优化 Python 代码能追上的。5.3 混合场景的折中策略实际项目里经常遇到“既要灵活又要性能”的情况我的经验是分层处理数据入口和出口用列表方便对接各种接口中间的计算密集段转成numpy或array算完再转回来。转换成本是一次性的换来的是中间段的性能提升整体算下来划算。另一个折中是“按需转换”只有当数据量超过某个阈值比如一万时才切换到紧凑容器小数据量直接用列表。这样既避免了小数据量的转换开销又保证大数据量不拖垮性能。这个阈值没有标准答案得根据你的实际压测结果定。6. 常见问题与排查实录6.1 类型混用导致的报错速查现象可能原因解决方向TypeError: an integer is requiredarray类型码与数据不匹配检查 typecode 和元素类型OverflowError数值超出类型码范围换更大的类型码如b换iValueError: cannot reshape元素总数与目标形状不符用-1让 numpy 推算或核对总数修改切片后原数组变了numpy 切片是视图需要独立副本时用.copy()二维列表改一个全变乘法初始化共享引用改用推导式逐行创建浮点结果有微小误差单精度或浮点累积误差换d或float64金额用整数分这张表建议收藏遇到报错先对号入座能省不少排查时间。6.2 三个我踩过的坑第一个坑是numpy的整数溢出。numpy默认的整数类型在 64 位平台上是int64但如果显式指定了int8或int32运算结果超出范围会静默回绕不报错。我曾经用int32存时间戳秒级跑到 2038 年附近就会溢出虽然当时没到那个点但这是个定时炸弹。后来统一改成int64踏实了。第二个坑是array的frombytes字节序问题。从文件或网络读来的二进制数据如果字节序和本机不一致frombytes出来的数值全是错的而且不报错。排查时用sys.byteorder确认本机字节序必要时用byteswap()转换。这个坑在处理跨平台数据时特别常见。第三个坑是列表推导式里的变量作用域。Python 3 里推导式的循环变量不会泄漏到外层这本来是好事但如果你在推导式里引用了外部同名变量容易看花眼。我的习惯是推导式里的临时变量用短名字如x、i外部变量用有意义的长名字从命名上就区分开。6.3 调试小技巧打印形状和类型处理数组类问题时我养成了一个习惯在每个关键步骤后打印形状和类型。def debug_arr(name, arr): print(f{name}: type{type(arr).__name__}, fshape{getattr(arr, shape, N/A)}, fdtype{getattr(arr, dtype, N/A)}, flen{len(arr)})这个函数在排查形状不匹配、类型意外转换时特别管用。很多numpy的报错信息只说“形状不匹配”但不告诉你具体是哪一步变的自己打印一下立刻定位。养成这个习惯之后我调试数组问题的时间至少缩短了一半。7. 一些让代码更稳的实操习惯写数组相关代码有几个习惯坚持下来能少踩很多坑。第一初始化可变容器一律用推导式不用乘法这条前面说过但值得再强调一遍因为它是最常见的低级错误来源。第二numpy里凡是需要独立副本的地方显式写.copy()别依赖“应该不会改到原数组”的侥幸心理。第三类型码和dtype在定义时就写明确别依赖默认推断默认值在不同平台、不同版本上可能有差异。还有一个习惯是“先小数据验证再上量”。数组类的问题往往在小数据量下不暴露一上量就崩。我的做法是先用几十条数据跑通逻辑确认形状、类型、边界都对再换成真实数据量跑。这样即使出问题排查范围也小得多。最后分享一个我常用的技巧需要频繁在列表和numpy之间转换时写一对辅助函数封装转换逻辑把类型检查和形状校验放进去。这样业务代码里只调辅助函数转换细节集中管理改起来也方便。这个封装看着多写了几行但在多人协作的项目里能避免大量“这个变量到底是列表还是数组”的沟通成本。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询