FEATURED · 精选文章

Python容器数据类型详解:列表、元组、字典与集合的实战使用指南

发布时间 / 2026/9/8 5:10:46
来源 / 创域科博编辑部
栏目 / 资讯中心
Python容器数据类型详解:列表、元组、字典与集合的实战使用指南 刚开始学Python的时候最容易让人产生成就感的不是print(Hello World)而是终于把多个数据装进一个变量里循环着处理它们。这部分能力靠的就是Python的容器数据类型。容器说白了就是能装东西的盒子列表、元组、字典、集合都是Python内置的几种容器。这篇文章我会从使用场景、操作方法到常见坑位尽量讲透适合刚学完基础语法、准备系统梳理容器用法的读者也适合写了一段时间代码、想回头补细节的朋友。1. 容器数据类型全景图先搞清楚有哪些盒子可用1.1 为什么编程语言需要容器如果你只需要处理一个学生成绩那一个变量就够了。但如果你要处理全班50个人的成绩总不能写50个变量叫score1、score2、score3……那代码基本没法维护。容器的作用就是把一组数据组织成一个整体让你用循环、索引、条件判断去批量操作。Python的容器设计得很贴近人的直觉。你说把苹果、香蕉、橘子放一个篮子里Python里就是 fruits [苹果, 香蕉, 橘子]。你说记录学号对应的姓名Python里就是 students {1001: 张三, 1002: 李四}。这种直觉化的设计是Python上手快的一个关键原因。除了组织数据容器还决定了你怎么去访问、怎么去改、怎么去算。比如列表里的东西有先后顺序按位置取字典里的东西按键取更像是查字典集合里的东西不重复天生适合做去重。选对容器写出来的代码不但短而且不容易出逻辑错误。1.2 四种核心容器的一次性对比Python内置的容器类型日常最常用的就是下面这四种容器类型写法示例是否有序是否可变是否允许重复典型用途列表 list[1, 2, 3]有序可变允许有序数据、频繁增删元组 tuple(1, 2, 3)有序不可变允许固定数据、函数返回值字典 dict{name: 张三}插入有序Python 3.7可变键不允许重复键值查找、映射关系集合 set{1, 2, 3}无序可变不允许重复去重、集合运算这里有个容易迷糊的地方什么时候用元组什么时候用列表简单说如果这个数据集合在程序运行过程中不应该被修改就优先用元组。比如一个点的坐标 (x, y)一个日期的 年月日这种天然固定的数据用元组语义更清晰还能防止不小心被改动。字典和集合都用花括号 {}但空花括号 x {} 创建的是字典不是集合。空集合要用 set() 创建这是个高频新手坑。我见过不少同学写 x {} 想表示空集合结果后面一调用 add 方法直接报错因为字典根本没有 add 方法。2. 列表和元组最常用的有序数据组2.1 列表的常用操作与实操细节列表大概是使用频率最高的容器。它最核心的特点有两个有序、可修改。创建列表的方式很多直接写中括号、用 list() 转换、用 range() 生成。# 常见创建方式 a [1, 2, 3] b list(hello) # [h, e, l, l, o] c list(range(5)) # [0, 1, 2, 3, 4]增删元素是列表的高频操作。add 方法列表里没有别搞混。列表常用的是 append、extend、insert、remove、pop。append(x)把 x 作为一个整体追加到末尾。extend(iterable)把可迭代对象里的每个元素逐个追加。insert(index, x)在指定位置插入。remove(x)删除第一个值等于 x 的元素如果不存在会抛 ValueError。pop(index)弹出并返回指定位置的元素不传参数默认弹出最后一个。append 和 extend 的区别特别容易踩。你写 a.append([4, 5])结果是 [1, 2, 3, [4, 5]]列表里套了一个列表你写 a.extend([4, 5])结果是 [1, 2, 3, 4, 5]。一个是塞整体一个是摊开来塞。切片是列表的精髓。切片语法 [start:stop:step]左闭右开也就是 start 位置包含stop 位置不包含。这个规则一开始容易别扭但记住取头不取尾就好了。nums [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] print(nums[2:5]) # [2, 3, 4] print(nums[:3]) # [0, 1, 2] print(nums[5:]) # [5, 6, 7, 8, 9] print(nums[::2]) # [0, 2, 4, 6, 8] print(nums[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]切片还有个隐藏技能可以批量替换元素。nums[1:4] [20, 30]原来的 1、2、3 位置会被替换成 20、30 两个元素列表长度会跟着变化。这种写法在算法题里处理区间替换非常好用。2.2 元组为什么比列表轻不可变的价值元组常被看成不能修改的列表但它不只是不能改这么简单。因为不可变元组可以安全地作为字典的键、集合的元素而列表不行——因为列表改了哈希值就会变会破坏数据结构的一致性。元组还有个很棒的用法叫拆包。一个函数返回多个值本质上返回的就是一个元组接收的时候可以直接拆开def get_min_max(nums): return min(nums), max(nums) low, high get_min_max([3, 1, 4, 1, 5]) print(low, high) # 1 5交换两个变量的值Python 里不用中间变量用的就是元组拆包a, b b, a。底部逻辑其实就是先构造一个元组 (b, a)再拆开赋值。创建单元素元组要注意逗号(5) 是整数 5(5,) 才是元组。这个细节面试和考试都常考。连接两个元组可以直接用加号但元组本身不可变——加号返回的是一个新元组不是修改原来的。性能上元组确实比列表轻一点。因为元组长度固定、结构简单Python 解释器可以做更多优化列表为了支持动态扩容会预留一些额外空间所以内存占用略高。如果你的数据量很大且确定不需要修改用元组能省一点内存。不过绝大多数场景下这种差异不至于成为决策依据更重要的还是语义对不对。3. 字典和集合键值对与去重利器3.1 字典的高效查找从查表到哈希字典是 Python 里查找速度最快的容器之一。它存储的是键值对查找的时候不用像列表那样一个个从头比而是通过键直接算出存储位置平均时间复杂度是 O(1)。这种数据结构底层叫哈希表Python 的字典和集合都是基于哈希表实现的。因为依赖哈希字典的键必须是可哈希的也就是不可变类型字符串、整数、元组都行列表、字典、集合不行。这一点写过几次自然会记住但新手经常困惑的是为什么我用元组当键有时候报错。比如 (1, [2, 3]) 这个元组里面包含列表整体就不可哈希了不能当键。字典的常用方法有几个使用频率特别高。d.keys() / d.values() / d.items()分别拿键、值、键值对视图。d.get(key, default)安全取值不存在时返回默认值而不报错。d.setdefault(key, default)键不存在时插入默认值存在则不覆盖。d.update(other)用另一个字典更新当前字典相同的键会被覆盖。d.pop(key, default)删除并返回对应值键不存在返回默认值。要特别强调视图对象的动态性。d.keys() 返回的不是静态列表而是一个视图它跟着字典一起变化。如果你先 k d.keys()然后 d[new] 1再遍历 k会发现 new 这个键也在里面。偶尔有同学以为 keys() 等于快照结果在遍历字典时又去修改字典直接抛 RuntimeError: dictionary changed size during iteration。遍历字典时最常见的做法是 for k, v in d.items()。不要写成 for k in d 然后每次 d[k] 取值那样也能跑但一个按键索引性能稍差代码也不够优雅。3.2 集合的操作与去重场景集合可以理解成没有值的字典它存了一堆不重复的元素。主要用途有两个去重集合运算。去重有个经典一行写法list(set(items))。但要注意这样去重后顺序不保证和原来一致因为集合本身是无序的。如果要求去重且保持原顺序可以这样items [3, 1, 3, 2, 1, 5] seen set() result [] for x in items: if x not in seen: seen.add(x) result.append(x) print(result) # [3, 1, 2, 5]这背后用到集合判断 in 是 O(1)的特性。注意这里 x not in seen如果 seen 是个列表那就是 O(n)数据量大了会非常慢。所以保持顺序去重用集合辅助是常规操作。集合运算是一般新手不常用但很强大的能力。交集、并集、差集、对称差集都有一行搞定a {1, 2, 3, 4} b {3, 4, 5, 6} print(a b) # {3, 4} print(a | b) # {1, 2, 3, 4, 5, 6} print(a - b) # {1, 2} print(a ^ b) # {1, 2, 5, 6}这个很适合处理两个名单里重复的人在A名单但不在B名单的人这类业务。另外 set 本身是可变的可以用 add、remove、discard 操作。discard 和 remove 的区别是删除的元素不存在时discard 不报错remove 抛 KeyError。如果你想做安全删除用 discard 更省心。还有一种不可变集合叫 frozenset一旦创建就不能修改。它和元组类似因为不可变所以可以作为字典的键也可以作为另一个集合的元素。应用场景不多但在做嵌套集合时很关键——set 里不能放 set但可以放 frozenset。4. 容器的高级操作与复制陷阱4.1 推导式一行代码生成新容器推导式是 Python 容器操作里特别有Python 味的语法。它本质上是一种从可迭代对象创建新容器的紧凑写法。最常见的是列表推导式squares [x * x for x in range(10)] # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] even_squares [x * x for x in range(10) if x % 2 0] # [0, 4, 16, 36, 64]基本结构是 [表达式 for 变量 in 可迭代对象 if 条件]。先写你想要的元素长什么样再写循环来源最后写过滤条件。如果元素本身就是元组比如 [(x, x*2) for x in range(3)]记得给元组加括号得到 [(0, 0), (1, 2), (2, 4)]不加括号可能被解释成别的意思。字典推导式和集合推导式思路一样squares_dict {x: x * x for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16} even_set {x for x in range(10) if x % 2 0} # {0, 2, 4, 6, 8}推导式虽然很爽但不要滥用。嵌套多层的推导式比如 [[a * b for a in range(5)] for b in range(5)] 还算能看再复杂下去比如三层以上循环还带条件阅读成本会很高。我个人的标准是如果推导式长度超过一行或者要费劲想两秒才能明白我宁可拆成普通 for 循环去写。代码是给人读的不只是给机器跑的。4.2 浅拷贝与深拷贝容器复制最大的坑先说个最常见的错误用等号复制列表然后改一个另一个也变了。a [1, 2, 3] b a b.append(4) print(a) # [1, 2, 3, 4]原因很简单b a 只是让 b 和 a 指向同一个列表对象并没有复制内容。真正想复制可以用 b a.copy() 或者 b a[:]。但 copy() 只是浅拷贝问题还在。如果列表里的元素本身是可变对象比如嵌套列表浅拷贝只复制了外层列表内层列表仍然共享a [[1, 2], [3, 4]] b a.copy() b[0].append(99) print(a) # [[1, 2, 99], [3, 4]]因为 b[0] 和 a[0] 指向同一个内部列表。这时候需要深拷贝from copy import deepcopyb deepcopy(a)。deepcopy 会递归复制所有层级的对象改哪个都不影响另一个。判断该用浅拷贝还是深拷贝核心就看容器里存的是什么。如果存的都是数字、字符串这类不可变对象浅拷贝就够了如果有嵌套的可变容器就需要深拷贝。默认情况下不建议一上来就 deepcopy因为深拷贝递归复制性能开销更大能浅拷贝解决的别深拷贝。这个坑在工作里经常遇到。比如配置字典是全局的某个函数只想基于它改一点点直接 dict2 dict1 然后上面改结果全局配置被污染了。排查半天才发现是引用共享问题。这种问题最好在写代码前就想清楚这里到底是共享引用还是真正复制一份。4.3 嵌套容器列表套字典字典套列表真实业务里很少有单一容器打天下的情况。一个学生管理系统往往是列表里套字典字典里套列表。比如students [ {name: 张三, scores: [88, 92, 75]}, {name: 李四, scores: [91, 83, 96]}, ] # 计算每个学生的平均分 for stu in students: avg sum(stu[scores]) / len(stu[scores]) print(stu[name], avg)嵌套容器访问的时候要一层层拆清楚。students[0][scores][1] 的意思是先取第一个学生再取他的 scores 字段再取第二个分数。写这种代码时我建议把每一层拆开用变量存而不是写一长串链式索引。比如 score_list stu[scores]不仅好读也方便后面判断空列表。嵌套结构里的修改要特别注意引用和复制的问题。比如循环里往一个大列表 append 一个字典如果每次 append 的是同一个字典对象后面改了字典之前 append 进去的也全部跟着变。正确的做法是每次循环创建新字典。这个问题我在实际项目里见过太多次而且表现很隐蔽数据量大时特别难查。5. 容器实战中常见的问题与排查5.1 典型异常与解决办法容器相关的报错翻来覆去就那几种。我把常见异常整理成一张表方便你遇到问题直接查。异常信息出现原因解决办法IndexError: list index out of range列表索引超出范围检查索引是否越界用 len() 判断或通过切片避免KeyError: xxx字典访问不存在的键用 get() 带默认值或先判断 key in dictValueError: list.remove(x): x not in listremove 一个不存在的元素删除前判断元素是否在列表里或用 discardRuntimeError: dictionary changed size during iteration遍历字典时增删了键先复制键列表 list(d.keys()) 再用或收集后统一改TypeError: unhashable type: list用列表当字典键或集合元素换成元组或 frozensetKeyError 是新手阶段最常碰到的。比如从接口拿到一个字典想取 res[data][name]但某个字段可能不存在代码直接崩。更稳妥的写法是 res.get(data, {}).get(name)每层都给个默认值这样缺字段也不会崩。遍历字典时修改字典的报错还有个典型的干净解法如果你想过滤掉某些键可以这样d {a: 1, b: 2, c: 3} for k in list(d.keys()): if d[k] % 2 1: del d[k] print(d) # {b: 2}关键在 list(d.keys())这一步把键快照成普通列表遍历快照改原字典就不报错了。5.2 容易被忽略的容器细节有几个容器细节平时写代码不一定会遇到但一旦遇到会卡很久。第一个是列表的乘法复制。a [[]] * 3 看起来创建了三个空列表实际上三个元素指向同一个内部列表。给 a[0].append(1)a 会变成 [[1], [1], [1]]。这是因为乘法重复的是引用不是深拷贝。如果你想要三个独立空列表得用 [[ ] for _ in range(3)]。第二个是 dict 的插入顺序。Python 3.7 开始官方保证字典保持插入顺序这让字典在一定程度上像有名字的列表。但不要依赖这个特性去做复杂顺序逻辑因为不是所有语言的字典都保证顺序跨语言迁移代码时容易出问题。第三个是集合和字典的性能前提。前面说字典查找 O(1)、集合去重快前提都是键是字符串、数字这类哈希成本低的类型。如果你自己定义了一个类作为键而且这个类的hash方法实现得很慢那性能优势会被抵消。还有千万不要在自定义类里让hash依赖可变属性否则对象放进集合后属性一变哈希值就变了集合就会出各种诡异问题。第四个是字符串也是容器。字符串可以切片、遍历、拼接算是一种特殊的不可变序列。不过在容器语境下它和列表最大的区别是不可变str[0] a 会直接报错。需要修改字符串时通常转成列表再操作list(s)改完再 .join(list)。第五个是 in 操作在不同容器里的性能差异。如果频繁判断某个元素是否在这个数据集合里不要用列表用集合。列表的 in 是 O(n)集合的 in 是 O(1)。说到这个我建议养成一个习惯只需要成员判断不关心顺序和重复就优先用集合数据规模一大性能差异非常明显。5.3 容器传参函数里改了会不会影响外部Python 函数传参是传引用但很多新手没意识到这个和容器结合时有多关键。如果你把一个列表传入函数函数里执行 append、remove、sort 这类操作外部列表会跟着变。比如def add_item(items): items.append(new) return items data [1, 2, 3] add_item(data) print(data) # [1, 2, 3, new]data 被改了。如果这不是你想要的在函数里 copies items.copy()对副本操作。反过来如果你故意想改造一个列表这种就地修改就是合理的比如自定义一个排序方法直接对传入列表排序。还有一个很经典的坑可变对象作为函数默认参数。def append_item(item, target[]) 这种写法target 默认列表只创建一次多次调用会把元素一直累积下去。正确做法是 targetNone函数里再判断def append_item(item, targetNone): if target is None: target [] target.append(item) return target这个问题在很多 Python 面试题里出现背后的原因就跟容器引用的生命周期有关。默认参数在函数定义时就被求值这个列表对象一直活着所以每次调用共享同一个列表。写在最后的个人经验容器这块内容刚开始学容易觉得琐碎一会儿有序一会儿无序一会儿可变一会儿不可变记不住。我的建议是别硬背多写。把每个容器类型都拿真实小任务练一遍用列表存购物车、用元组固定坐标、用字典做成绩映射、用集合去重名单。用着用着哪个场景选哪个容器就成了本能反应。我实际写代码这几年感受最深的一点是容器选择决定了代码的简洁程度和运行效率。同样是成员判断用列表和用集合在大数据量下可能就是秒级和毫秒级的差别。同样是复制容器搞不清浅拷贝和深拷贝线上数据被污染了都不知道从哪查起。所以别觉得这些是基础就轻视越基础的东西翻车成本越高。最后分享一个小技巧调试容器问题时如果一时看不出来哪里改了数据就在关键操作前后打印 id()看是不是同一个对象。id 不同说明是新对象id 相同说明是原地修改能帮你快速定位 怎么改着改着别的地方也变了 这类问题。这个办法很土但真的很管用。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻