新闻详情

新闻详情

首页 / 资讯中心 / 详情

Python四种内置数据结构详解:列表、元组、字典与集合实战选型

发布时间:2026/9/25 4:23:14来源:尧图网络
Python四种内置数据结构详解:列表、元组、字典与集合实战选型
刚入行那年我写过一段自己都不忍心看的代码临时记录用户数据全塞进列表后续按订单号找人直接遍历列表匹配。数据量小的时候没什么感觉等跑到几百万条那个查询慢得让我怀疑人生。后来我把这四种内置容器——列表、元组、字典、集合——从头到尾梳理了一遍才知道问题不在于 Python 慢而在于我压根没选对数据结构。这篇文章不是教你背 API而是把我实际项目里用这些数据结构的经验整理出来它们各自适合什么场景、底层为什么快、哪些坑一定绕不开以及怎么从需求直接选到正确的容器。无论你是刚学到这几种类型的小白还是写了几年 Python 想系统梳理一下这篇应该都能给你一点参考。1. 先理清四兄弟的根本差异可变性、顺序性、存储方式很多教程喜欢直接甩出一张方法表让读者硬背。我觉得顺序反了应该先理解这四种结构“天生”的差异——它们决定了你能对容器做什么、不能做什么、底层花了多少代价。1.1 先看一眼它们在代码里的基本形态# 列表一列有顺序的“物件”随时可以增删改 fruits [apple, banana, cherry] fruits.append(orange) # 元组一列有顺序但“固死”的物件 point (3, 5) # 字典用键找值而不是用位置找值 user {name: 张三, age: 30} # 集合一堆互不相同的“物件”不分先后 tags {python, rust, go}形态上争议不大但真正决定它们性格的是下面这张表里的三个维度类型是否有序是否可变元素可重复元素是否必须可哈希典型用途列表 list是是是不要求动态序列、遍历、索引元组 tuple是否是是元素均不可变时固定结构、打包返回、字典键字典 dict3.7 保留插入序是键值可变键必须唯一键必须可哈希键值映射、快速查找集合 set否是否自动去重必须可哈希去重、集合运算、成员判断注意最后两列字典的“键”、集合的“元素”都必须是可哈希的而列表、元组、字典、集合在元素类型上都没有这个限制。换句话说字典和集合在“往里放什么”这件事上比列表和元组严格得多。1.2 三个关键词决定它们的性格可变还是不可变。列表、字典、集合可以原地增删改元组不行。这个差异不是“Python 故意为难你”而是设计意图可变容器适合做需要动态维护的数据不可变容器适合做“定下来就不再变”的数据结构。函数返回一个坐标(x, y)用元组数据流里不断收到的订单用列表这是很自然的分工。有序还是无序。列表和元组是有序序列严格保持元素的先后位置字典从 Python 3.7 起保留键的插入顺序但注意这种“保留插入序”和排序没有任何关系它只保证你插入的顺序能被读出来集合则是彻底无序你看到输出顺序只是当前哈希布局的副产品随时可能变。按“位置”找还是按“名字”找。列表和元组使用整数下标定位适合“第几个元素”这种访问方式字典用键定位适合“名字→值”这种映射关系集合不提供取值接口它只回答“某某元素在不在里面”这种问题。1.3 一个很容易混淆的“有序”概念网上经常有人问“python 列表是有序序列吗”“字典现在是不是有序了”。我的回答是列表的“有序”是序列意义上的有序它跟你放入元素的先后、以及后续排序都强相关字典的“有序”是插入序意义上的有序Python 保证你能按插入顺序遍历但不保证键之间有任何大小或逻辑上的次序。如果你需要按键排序得自己sorted(d.items())如果需要按值排序还得自己指定 sort key。别把“保留插入顺序”误当成“自动排序”这是初学阶段最常踩的认知坑。集合就更直接了它必须无序因为哈希表的布局优化会随时改变遍历顺序如果同一份数据两次遍历结果不同这不叫 bug这正是集合的特性。2. 列表和元组同为序列性格却完全相反列表和元组经常被放在一起比较因为它们的表面太像了都能按下标访问、都能切片、都能遍历。真正拉开差距的是可变性。2.1 列表的“好动”是天生的方法设计围绕增删改查列表是 Python 里最通用的工作马我一天写的代码里列表至少占一半。常用的操作无非是这几个方向items [1, 2, 3] # 增 items.append(4) # 末尾追加 items.extend([5, 6]) # 合并另一个序列 items.insert(1, 99) # 指定位置插入 # 删 items.pop() # 弹出末尾元素 items.pop(0) # 弹出指定位置 items.remove(99) # 按值删除第一个匹配项 items.clear() # 清空 # 改 items[0] 100 # 按下标覆盖 items.sort(reverseTrue) # 原地排序为什么列表适合频繁增删改因为它在内存里是一段连续空间CPython 里是动态数组配合额外的容量预留append平均是 O(1) 的非常快。但要注意insert和pop(0)这类在头部操作的动作会让后面所有元素整体移动数据量大时 O(n) 的成本会显出来。如果你确实需要频繁从两端操作建议换成collections.deque。列表的另一大特点是可以容纳异构对象[1, a, None, [2, 3]]完全合法。这让它特别适合做“还没定型的数据集合”比如从接口解析出来的原始结果你往往先丢进列表再慢慢整理。2.2 元组不可变不是限制而是契约元组的核心价值就一个字稳。不可变带来的好处是实打实的能当字典的键哈希表要求键在生命周期里不变元组满足这个条件列表不行。能做集合的元素{(1, 2), (3, 4)}合法{[1, 2]}直接报错。能防意外修改别人拿到你的元组想改也改不了减少耦合风险。性能略好元组结构更紧凑迭代和打包解包都更省。实际开发里用的最多的是函数多值返回。Python 里写return x, y本质上就是返回一个元组接收时a, b foo()再解包。这个语法糖用完就忘但它背后依赖的正是元组的“整体性”——一个返回多个值的函数返回值是一个不可拆散的整体。2.3 切片、namedtuple还有元组里藏着的“可变量陷阱”列表切片是高频操作先把核心规则记住含头不含尾。nums [0, 1, 2, 3, 4, 5] nums[1:4] # [1, 2, 3] nums[:3] # [0, 1, 2] nums[2:] # [2, 3, 4, 5] nums[::2] # [0, 2, 4] nums[::-1] # [5, 4, 3, 2, 1, 0] 反转切片返回的是新列表所以nums[:]是浅拷贝的经典写法。这点很重要后面讲拷贝坑的时候还要提到。如果觉得point[0]、point[1]这种写法可读性太差可以给元组加名字from collections import namedtuple Point namedtuple(Point, [x, y]) p Point(10, 20) print(p.x, p.y) # 10 20namedtuple本质还是元组性能不可变性的好处全保留只是多了属性访问的便利。处理坐标、表格行、配置项这类“固定字段”的数据时非常好用。最后必须说一个坑元组的“不可变”是表面的。如果元组里装了一个列表这个列表的内容是可以改的t (1, 2, [3, 4]) t[2].append(5) # 不报错 print(t) # (1, 2, [3, 4, 5])元组只是管住了“元素的指向”管不住“元素自己的内部状态”。如果拿这样的元组去当字典键照样会出问题因为它实际上不是稳定的。所以只有当元组里所有元素都不可变时它才是真正可靠的哈希键。3. 字典和集合哈希表背后的空间换时间列表和元组是一对“序列”字典和集合则是一对“哈希表兄弟”。理解了哈希表你才算真正看懂了这两个数据结构。3.1 字典为什么查得快把名字算成位置很多人第一次用字典没觉得有多厉害直到数据量上来了才意识到列表查一个元素要从头扫到尾字典却能直接命中。原理其实不复杂。字典内部维护一张哈希表。存键值对时先对键做哈希运算得到一个整数再用这个整数找到存储位置。取回时也一样对键做哈希直接去对应位置拿值。整个过程不依赖数据总量所以平均时间复杂度是 O(1)和列表的 O(n) 扫描不在一个量级。这就是典型的“空间换时间”哈希表往往要预留比实际数据更多的槽位来减少冲突、提升查找效率。常见的哈希冲突处理是开放寻址或者链地址法CPython 的字典用的是更精细的紧凑哈希表实现但核心思想不变——用哈希函数把“任意键”映射成“数组下标”。正因如此键必须可哈希。列表、字典、集合都不能当键因为它们的内容会变一旦变了再算哈希就找不到原来的位置数据就丢了。3.2 可哈希的门槛为什么列表不能当键# 合法 d {(1, 2): a, name: b, 42: c} # 非法 # d {[1, 2]: a} # TypeError: unhashable type: list元组能当键的条件是内部元素也全部可哈希。一个元组里套了列表那它整体就不可哈希。这个限制不是随便定的它保障了哈希表的正确性。你可以这样理解哈希表相当于把每把钥匙拍成照片存起来钥匙不能在存进去之后换造型否则照片就对不上了。列表可变所以不能当钥匙元组不可变所以可以。3.3 集合只看“有没有”不看“在哪”集合本质上就是“只存键不存值”的哈希表。你往集合里放元素它只关心这个元素存不存在不提供下标访问也不提供键值对应。因为底层是哈希表集合天生做了两件事自动去重相同元素算出的哈希一样第二次放不进去。成员判断极快x in s在集合里是 O(1)在列表里是 O(n)。集合的另一大杀器是数学运算处理标签、分类、权限这类数据非常顺手a {python, go, rust} b {go, java} a b # {go} 交集 a | b # {python, go, rust, java} 并集 a - b # {python, rust} 差集 a ^ b # {python, rust, java} 对称差集我经常用集合做这样的事一份用户名单里找出同时在两个班的人、审计两份日志文件里出现过哪些相同 IP、或者快速确认某个订单号是不是黑名单里的。先转成集合再算代码写起来短性能还比循环内嵌套判断好得多。3.4 字典的顺手 APIget、setdefault、defaultdict、Counter新手最常见的写法是查字典前先判断键在不在if score in student: score student[score] else: score 0其实get一行就能解决score student.get(score, 0)要“取出值没有就初始化成空容器再往里塞东西”的场景用setdefault或者defaultdict都行data {} data.setdefault(group_a, []).append(张三) from collections import defaultdict groups defaultdict(list) groups[group_a].append(李四)两者的差别在于defaultdict是用字典的子类自动处理缺失键更省代码也更直观setdefault则是标准字典自带的能力。我喜欢在需要清晰展示输出结构时用setdefault在快速处理数据时直接上defaultdict。统计元素数量更是简单from collections import Counter cnt Counter(abracadabra) print(cnt) # Counter({a: 5, b: 2, r: 2, c: 1, d: 1})Counter 本身就是字典子类但多出了most_common()这类便捷方法做词频统计、商品销量排行时一行搞定。4. 真实场景里的选型思路从需求直接定位容器数据结构选型不是看哪个“高端”而是看你要回答什么问题。这个问题问对了答案几乎自己会浮现出来。4.1 先回答三个问题再选结构问自己三个问题基本能把四种结构定位到一两个候选人我要不要保持顺序要而且后面可能增删改 → 列表要但定下来就不变 → 元组不要但我要按名字取值 → 字典不要我只要“存不存在、有哪几个唯一值” → 集合我要不要修改内容要 → 列表、字典、集合不要 → 元组这也是传参、返回值偏爱元组的原因我的访问方式是按位置、按名字还是按唯一性按下标或迭代 → 列表按固定字段名 → 元组 namedtuple按键 → 字典只判断成员关系和去重 → 集合4.2 典型场景拆解从需求到结构的映射实际需求推荐结构理由日志按时间顺序追加并滚动展示列表有序、动态、append 效率高函数返回坐标、状态码、错误信息元组固定结构、解包方便、不可变用用户名快速查用户资料字典O(1) 查找天然映射关系从 10 万元素里找出重复项集合哈希去重比手动计数快得多两个班级名单求交集集合运算一行完成统计一篇文章每个单词出现次数字典 Counter键值映射天然适配计数上面这些场景我在项目里基本都走了一遍日志系统用列表攒数据接口响应解析用列表套字典权限校验用集合做交集用户画像查询用字典做缓存。每次选型都先问“我到底要按什么访问数据”而不是“我熟哪个就用哪个”。4.3 嵌套设计的两个方向列表套字典还是字典套列表实际开发里容器不会单独出现嵌套才是常态。两种最常见的嵌套方向各有优劣。列表套字典适合“一列同类的对象”。比如接口返回多个用户users [ {name: 张三, age: 30}, {name: 李四, age: 25}, ]这种结构遍历展示、渲染表格、按索引取某条记录都非常顺利。缺点是如果按名字查用户得每次遍历效率不如字典套字典。字典套列表适合“按一个键分组每组多个值”。比如按部门收集员工from collections import defaultdict department defaultdict(list) department[研发].append(王五) department[研发].append(赵六) department[市场].append(孙七)这种结构天然适合分组统计一次遍历就能把数据归好类。缺点是如果要知道“某个人属于哪个部门”还是要遍历所有组本质上这是一张“组→成员”的映射而不是“成员→组”的映射。判断原则其实还是那一句你最频繁的访问方式是什么。频繁按索引遍历就列表套字典频繁按键聚拢就字典套列表。别一开始就整花活先把访问热点定下来结构就定了。5. 四个高频坑拷贝、默认参数、遍历删除、in 判断这几类问题我几乎每年都会在代码 review 里碰到几次。每次看着都眼熟改起来也都是一两行的事但理解不深的人下次还会踩。5.1 浅拷贝和深拷贝为什么直接赋值还是被改了先记住一个结论直接赋值等号没有复制任何东西只是给同一个对象多起了个名字。a [[1, 2], [3, 4]] b a # b 和 a 是同一个对象 b[0].append(99) print(a) # [[1, 2, 99], [3, 4]] a 也被改了要用拷贝列表有copy()、切片[:]字典有dict.copy()集合有set.copy()。但这些都是浅拷贝——只复制最外层容器嵌套的列表、字典仍然是共享的a [[1, 2], [3, 4]] c a.copy() # 外层是新列表内层还是同一个列表 c[0].append(99) print(a) # [[1, 2, 99], [3, 4]] a 还是被改了如果内部结构也有可变对象需要的是深拷贝import copy d copy.deepcopy(a) d[0].append(50) print(a) # 完全不受影响我的经验是默认情况下尽量用不可变结构元组来降低拷贝风险不得不拷贝嵌套可变结构时先想清楚“我只改外层还是连内层一起复制”——浅拷贝省内存省时间深拷贝更安全但复制大对象时会慢得多。5.2 默认参数用可变对象一个隐蔽的“记忆”bug这是一个非常反直觉的经典坑。函数定义的时候默认参数只被求值一次之后每次调用都用同一个对象def add_item(item, lst[]): lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [1, 2]第二次调用时lst已经不是空列表了而是上一次调用后的残留。这在多线程、多次调用的场景里会制造非常诡异的问题——明明每次传参都是默认空列表结果数据越积越多。解决办法是默认参数写None函数内部再初始化def add_item(item, lstNone): if lst is None: lst [] lst.append(item) return lst为什么不用可变默认值因为这个默认值属于函数对象本身不属于某一次调用。无论调用多少次读到的都是同一份“历史存档”这通常不是你想要的行为。5.3 遍历时删除元素为什么结果总是差那么一点循环里直接删列表元素是我见到最多的逻辑 bug 之一。问题出在遍历基于“当前位置”删除会让后面元素整体前移导致跳过下一项lst [1, 2, 2, 3] for n in lst: if n 2: lst.remove(n) print(lst) # [1, 2, 3] 第二个 2 被跳过了正确做法通常有三种倒序遍历、先复制再删、用列表推导式重建。其中列表推导式最 Pythoniclst [1, 2, 2, 3] lst [n for n in lst if n ! 2] print(lst) # [1, 3]字典和集合在遍历过程中也不允许改变大小否则直接抛异常d {a: 1, b: 2} # for k in d: # d.pop(k) # RuntimeError: dictionary changed size during iteration这种时候要先拿到键的副本或者直接用字典推导式重新构造d {k: v for k, v in d.items() if v ! 99}我个人的习惯是需要遍历过滤时第一时间想“生成一个新容器而不是原地删”。不仅安全可读性也高得多。5.4 in 判断的性能差别为什么“先 set 再查”能快这么多很多人在循环里高频使用if x in my_list数据量一上来就慢得离谱。原因前面已经说过列表的in是顺序扫描 O(n)集合和字典的in是哈希查找 O(1)。import timeit lst list(range(10000000)) st set(range(10000000)) target 9999999 lst_time timeit.timeit(lambda: target in lst, number100) set_time timeit.timeit(lambda: target in st, number100) print(flist in 耗时: {lst_time:.4f}秒) print(fset in 耗时: {set_time:.4f}秒)在我机器上跑差距通常能到两三个数量级数据量越大越夸张。所以一旦发现某段代码里in list是热点先把容器转成集合或者字典效果立竿见影。反过来也提醒一件事所有方便都是有代价的。集合查得快但它无序、元素必须可哈希、占用空间也更大。如果数据量很小几十上百个列表和集合的差别完全可以忽略别为了炫技过度设计。6. 性能实测与我的最终建议前面讲了很多原理这一节我想从“实践数据”的角度再说说选型时怎么权衡。6.1 用 timeit 感受一下容器的真实成本如果你还没有用过timeit建议现在就去跑跑看。它的价值在于把直觉变成数据。除了上面说的in判断还有几个常见操作也值得测一测列表pop(0)和pop()的差别头部弹出要整体移动尾部弹出几乎不花钱。字典正常取值和get的差别基本一致get的额外参数只是兜底开销很小。在大列表里sort和转成集合再去重再排序的差别很多时候直接在列表上sorted(set(lst))反而更快更短。跑这些测试不是为了记住常数而是建立一种“容器成本意识”。我在优化线上脚本时最常用的手法就是先用cProfile找出热点函数然后看它里面容器操作是哪些再把高频访问的数据从列表换成集合或字典。大部分时候改完效果很明显。6.2 空间和速度的权衡什么时候用列表也够别误会我不是劝你所有场景都上集合和字典。哈希表虽然查得快但是每个元素都要算哈希、要维护哈希索引内存占用比连续数组的列表高很多。如果容器里元素很少、循环次数也不多列表的简单反而更合适。我自己的判断标准是数据量超过三五千、并且成员判断或按键查找是高频操作时才值得在结构和内存上多花钱数据量小的时候怎么简单怎么来。过去几年里我见过不少“硬把列表换成集合”的方案最后不仅代码变绕了性能也没提升多少。选型是对需求的响应不是对名词的崇拜。6.3 个人经验把四兄弟用顺的几条原则做了这么多年开发我对四种内置容器的使用经验可以浓缩成几句话第一先问访问方式再选结构。按位置访问用列表或元组按名字访问用字典按存在性访问用集合。这个问题在选型里解决八成问题。第二能不改就不改优先用不可变结构。日常定义常量、返回多值、传参时元组比列表更稳。它省心的地方不是性能而是“不会被我自己的后续代码改坏”。第三怀疑性能时先测再改。用timeit量化对比用cProfile找热点不要靠感觉优化。我见过太多人凭直觉把列表改成字典结果发现瓶颈根本不在这里。第四把所有转换技巧记牢list()、tuple()、set()、dict()四个构造函数可以互相转换sorted(set(lst))能快速去重排序dict(zip(keys, values))能快速造字典。这些一行式在数据处理里非常常用等于让你在四种容器之间随意穿梭。最后再分享一个小技巧排查数据相关 bug 时别死盯着代码逻辑先打印容器里不得了的东西。把type(x)、id(x)打印出来很多“为什么我的数据被改了”的问题一下子就清楚了。Python 的容器模型里引用和复制是两回事这个意识越早建立你踩的坑会越少。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

MOS管从入门到精通:NMOS/PMOS开关电路设计与选型避坑指南 2026/9/25 4:53:58

MOS管从入门到精通:NMOS/PMOS开关电路设计与选型避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
ESP32 -O2崩溃根因解析:编译器优化与嵌入式代码可靠性 2026/9/25 4:53:58

ESP32 -O2崩溃根因解析:编译器优化与嵌入式代码可靠性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
特斯拉HW4.0硬件深度拆解:11摄像头+4D雷达如何重塑自动驾驶感知 2026/9/25 4:53:52

特斯拉HW4.0硬件深度拆解:11摄像头+4D雷达如何重塑自动驾驶感知

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
C++中^不是次方:幂运算的正确姿势与避坑指南 2026/9/25 4:53:52

C++中^不是次方:幂运算的正确姿势与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
C# WinForm流程图控件源码解析:GDI+绘制、拖动与序列化全实现 2026/9/25 4:53:52

C# WinForm流程图控件源码解析:GDI+绘制、拖动与序列化全实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Wormhole勒索病毒深度分析:蠕虫式横向传播与应急响应实战 2026/9/25 4:53:52

Wormhole勒索病毒深度分析:蠕虫式横向传播与应急响应实战

1. 一次真实的应急响应:从一台中招机器说起凌晨两点被电话叫醒,对方是合作公司的运维负责人,语气很急——财务共享盘里所有文件后缀全变了,桌面上多了一个文本文件,里面写着要联系某个邮箱、支付一笔加密货币。我让他先…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉