尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Python核心容器详解:序列、字典、集合的原理、操作与实战

发布时间:2026/9/23 21:16:57

资讯中心
01
ARTICLE

Python核心容器详解:序列、字典、集合的原理、操作与实战

Python核心容器详解:序列、字典、集合的原理、操作与实战
Python 学了一段时间写过变量、分支、循环、函数之后你八成会撞上同一个问题数据多了往哪放一堆人名、几组成绩、多段文本总不能一个变量一个变量地存吧。这时候就得认识 Python 里三张王牌——序列、字典、集合。这三样东西几乎承包了日常开发里 90% 的数据组织工作也是很多 Python 入门教程一定会单开一章讲透的内容。这篇博文就围绕序列、字典、集合三个核心容器把它们的原理、常用操作、典型坑位一次说清楚配合可以直接复制运行的代码片段适合正在啃 Python 基础、准备应付面试笔试题、或者想把自己写脚本的数据结构意识提上来的朋友。先说个方向这篇文章默认你已经装好了 Python 3.7 以上版本并且会写最基本的 for 循环和函数。如果连解释器都没装建议先去把环境配好再回来看容器效果会好很多。我不会跟你念文档只会把这些年实际写代码时真正高频用到的部分拎出来讲。1. 先想清楚序列、字典、集合分别是什么角色很多新手学容器上来就背“列表用 append、字典用 get、集合用 add”背完就忘因为脑子里没有一张完整的图。我建议先把三兄弟的分工搞清楚序列是“有顺序的一排座位”字典是“名字查值的一本通讯录”集合是“一兜互不重复的元素”。后面的所有细节都是围绕这三句话展开的。1.1 三种容器的底层设计差异序列包含列表、元组、字符串、range 这几类核心特征是元素有先后顺序可以通过下标访问。你可以把序列想象成电影院的座位一排座位有固定编号你只要报出第几排第几座就能直接找到那个人。字典就完全不一样了。它是通过键key来找值value底层是一张哈希表查找的时候不需要从头扫到尾。它更像储物柜你记住柜子编号走到对应柜门前打开就行不需要一个个柜子试过去。集合从结构上来说是“没有值的字典”最擅长的两件事是去重和判断元素是否存在。它同样是哈希表实现所以判断一个元素在不在集合里速度接近 O(1)和集合里存了多少元素基本无关。三者的共同点也很重要它们都是容器都可以用 len() 取长度都可以用 in 做成员判断都可以直接放进 for 循环里迭代。共同点保证了你在写代码时只需要记一套遍历习惯就够了。1.2 一张表看懂三大容器容器类型是否有序是否可变是否能当字典的键典型使用场景list 列表有序可变不能动态数据集合、按位置存取tuple 元组有序不可变能固定数据、函数返回多值str 字符串有序不可变不能文本处理range有序不可变不能数值序列、循环次数控制dict 字典插入有序可变不能键值映射、查找表、缓存set 集合无序可变不能去重、关系运算、快速判断frozenset 冻结集合无序不可变能作为字典键、作为集合的元素注意看“是否能当字典的键”这一列。能当键的对象必须可哈希也就是说它不能变。元组只要里面装的元素也都是不可变的就能当键列表不行因为列表可以变哈希值没法稳定。这一点后面讲字典坑位的时候还得反复提。1.3 常见误区列表是有序序列吗直接说结论列表是序列有顺序性但不保证“按值排序”。很多刚入门的朋友被“有序”这俩字坑了以为有序就是从小到大排好序了。有两个概念要分清顺序性元素之间有确定的先后位置能通过索引访问。列表、元组、字符串、range 都满足。排序性元素的值按某种规则排好比如从小到大、从大到小。这只是一种特殊的顺序。所以“Python 的列表是有序序列”这句话指的是你能用lst[0]、lst[-1]按下标取东西不是说它自动帮你排好序。想要排序还得自己调用sort()或sorted()。而集合虽然底层也有顺序但那是哈希决定的人类看不懂也不该依赖所以一般都说是无序的。2. 序列专题列表、元组、range 的干活细节序列是三大容器里内容最杂的一组因为列表、元组、字符串、range 都在这个阵营里。这里我挑最常用的三类展开字符串虽然也是序列但它水太深建议以后单独学字符串方法。2.1 列表动态数组的脾气列表在 Python 底层的实现是一个“动态数组”准确说是一个存放对象引用的数组。动态的意思是它不需要你提前声明长度往里面 add 的时候会自动扩容。append这个操作均摊下来是 O(1)非常快但如果你在列表中间位置insert或del后面的元素全部要往前或往后挪一位这就是 O(n)。可以这么理解电影院里突然往中间排塞了一个人后面一排人都得挪屁股。基础操作给你列一份最常用的lst [3, 1, 2, 2, 4] lst.append(5) # 末尾追加结果是 [3, 1, 2, 2, 4, 5] lst.extend([6, 7]) # 拼接另一个列表 lst.insert(0, 0) # 在下标0处插入结果是 [0, 3, 1, 2, 2, 4, 5, 6, 7] lst.remove(2) # 删除第一个值为2的元素注意不是按下标删 lst.pop() # 弹出并返回末尾元素 lst.pop(0) # 弹出并返回下标0的元素 lst.index(3) # 返回值为3的首次出现下标 lst.count(2) # 统计2出现了几次 lst.sort() # 原地排序 lst.reverse() # 原地反转列表推导式是 Python 的招牌写法几乎所有新手都应该尽早掌握。它能把一个“循环 append”的代码压成一行# 取出0到19之间所有偶数的平方 squares [x * x for x in range(20) if x % 2 0]这段代码等价于squares [] for x in range(20): if x % 2 0: squares.append(x * x)初看觉得省不了多少但代码量上到几十行的时候推导式不仅省行数还更贴近“我要什么结果”的思维方式。2.2 切片Python 最优雅的细节切片是序列类型独有的能力语法是seq[start:stop:step]位置参数可以省略。几个比较高频的写法nums [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] nums[2:5] # [2, 3, 4]注意 stop 不包含 nums[:3] # [0, 1, 2]从头开始 nums[7:] # [7, 8, 9]一直到尾 nums[::2] # [0, 2, 4, 6, 8]每隔一个取一个 nums[::-1] # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]反转 nums[-3:] # [7, 8, 9]从倒数第3个一直到末尾负索引是切片里特别容易懵的点。-1表示最后一个元素-3表示倒数第三个所以nums[-3:]就是取最后三个。写的时候注意别让 start 和 stop 的边界打架最稳妥的办法是先想清楚“我要从哪到哪”。一个很重要的细节切片返回的是一个新的容器对象尤其是列表切片是浅拷贝。也就是说b a[:]得到的是另一个列表改 b 不会影响 a。但如果是嵌套列表比如列表里套着列表浅拷贝只会复制外层引用内层列表还是同一个对象。这个坑后面单独说。切片还有一个隐藏能力赋值。你可以用切片一次性替换一段lst [1, 2, 3, 4, 5] lst[1:4] [20, 30] # 结果 [1, 20, 30, 5]这个操作看起来像列表赋值其实是用右边的内容替换掉 [1,4) 这一片。长度不匹配也能替换因为这是“替换”而不是“对应赋值”。2.3 元组不可变的可靠搭档元组和列表长得很像区别是元组一旦创建就不能修改。定义元组用圆括号而且特别注意单元素元组的写法(1,)那个逗号不能省。不可变到底带来什么好处我总结三个点第一元组可以哈希所以能当字典的键。比如你要用坐标(x, y)作为键去存数据就必须用元组。用列表会直接抛TypeError: unhashable type: list。第二元组不容易被误改。多人协作或者代码逻辑复杂的时候一个不该变的配置数据用元组存等于给代码加了一道保险。第三元组的性能略好、内存占用略小。虽然这差距在大多数场景下可以忽略但在循环里创建大量小对象时元组会比列表快一点。元组最实用的场景是解包point (3, 4) x, y point # 拆成两个变量 a, b b, a # 交换变量不用临时变量 for index, value in enumerate(lst): # 循环里拆包 print(index, value)解包看着像小技巧其实在日常写代码里非常常见尤其是函数返回多个值时配合元组解包可以把代码写得很干净。2.4 range 与序列通用操作range不是列表而是一个惰性序列。它记下起始值、终止值和步长等你要数据的时候才一个个生成所以占内存很小。range(1000000)开一千万个数也不会卡死但如果你真把它转成 list那内存消耗就很可观了。list(range(5)) # [0, 1, 2, 3, 4] list(range(2, 10, 2)) # [2, 4, 6, 8] list(range(10, 0, -1)) # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]序列类型还有一些通用操作不管你是列表、元组还是字符串都能用len(s) # 元素个数 max(s) # 最大值 min(s) # 最小值 sum(s) # 求和 x in s # 判断成员 s.count(x) # 统计出现次数 s.index(x) # 找下标写代码的时候如果一个操作对列表、元组、字符串都适用那就记通用版本能少记一半的 API。这也是为什么我把这些操作单独拎出来讲你后面写算法题会非常依赖它们。3. 字典专题哈希表做成的“查表神器”字典可能是三种容器里业务价值最高的一个。原因很简单现实世界里的数据大部分都是“键-值”结构人是靠名字找电话号码商品是靠编号找库存配置项是靠配置名找值。字典就是 Python 专门为这种场景设计的。3.1 字典查找为什么那么快想要真正理解字典必须懂一点哈希表的概念。你往字典里放一个键值对时Python 会先调用hash(key)把键转成一个整数这个整数经过某种映射后定位到内部数组的一个位置值就存在那里。等你用键去取值时还是先算哈希、再定位整个过程不需要和其他键比较。所以理想的哈希表查找时间是 O(1)跟字典里有多少数据没关系。那要是两个不同的键算出了同一个位置怎么办这叫哈希冲突。Python 解决冲突的方式是不断探测下一个空闲位置所以性能不会出现断崖式下降只是多花几次探测。哈希表也带来了一个硬性要求键必须是可哈希的。不可变对象基本都可哈希比如整数、字符串、元组可变对象基本不可哈希比如列表、字典、集合。这也就是为什么“用列表当字典键”会报错。我把哈希表类比成图书馆的索书号你知道书号直接去对应书架找书不需要把整个图书馆翻一遍。这就是字典比列表查找快的原因也是后来集合判断成员很快的根源。3.2 字典基本操作从创建到遍历创建一个字典有好几种姿势d1 {name: 张三, age: 18} d2 dict(name李四, age20) # 关键字传参键必须是合法标识符 d3 dict([(a, 1), (b, 2)]) # 从键值对列表转换 d4 dict.fromkeys([a, b], 0) # 批量创建默认值都是0取值和设值d {name: 张三, age: 18} d[name] # 张三 d[score] # KeyError键不存在会报错 d.get(score, 0) # 0键不存在返回默认值 d.setdefault(score, 100) # 键不存在就设置默认值并返回 d[age] 19 # 修改已有键的值 d.update({city: 北京, age: 20}) # 批量更新/新增删除操作del d[age] # 删除指定键不存在就 KeyError d.pop(age) # 删除并返回值 d.pop(city, None) # 给默认值不报错 d.popitem() # 删除最后一个键值对返回 (key, value) d.clear() # 清空遍历是字典的拿手好戏for key in d: # 只遍历键 print(key) for value in d.values(): # 只遍历值 print(value) for key, value in d.items(): # 同时遍历键和值推荐这种 print(key, value)这里有个心得除非你明确只需要键或只需要值否则一律用for k, v in d.items()因为只遍历键时如果想拿值还得再查一次字典白白多一次哈希查找。直接遍历 items 是最省事的。3.3 视图对象、字典推导式与合并d.keys()、d.values()、d.items()这三个方法返回的不是普通列表而是视图对象。视图的特点是它会动态反映字典的变化。你先把视图存到一个变量里然后往字典里加键值再去看视图会发现新键已经出现了。这个行为有时候是优点有时候是坑知道它存在就好。字典推导式和列表推导式写起来很像squares_dict {x: x * x for x in range(10)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81} names [张三, 李四] scores [89, 96] score_dict {name: score for name, score in zip(names, scores)}字典合并也有很简洁的写法。Python 3.9 以后支持用|运算符d1 {a: 1, b: 2} d2 {b: 3, c: 4} merged {**d1, **d2} # {a: 1, b: 3, c: 4} merged d1 | d2 # 3.9 的写法结果相同注意后写的字典会覆盖先写的所以上面合并结果里b是 3不是 2。这个顺序是高频考点也是实际写代码容易忽略的细节。3.4 字典实战词频统计、缓存与映射字典最常见的实战场景是统计。比如统计一段文本里每个单词出现的次数新手最容易写的代码是text apple banana apple orange banana apple words text.split() count {} for word in words: if word in count: count[word] 1 else: count[word] 1 print(count)这段代码能跑但写起来啰嗦。可以改用setdefault或者defaultdictfrom collections import defaultdict count defaultdict(int) for word in words: count[word] 1defaultdict(int)的意思是访问一个不存在的键时自动先调用int()生成默认值 0所以count[word] 1第一次执行时就是 0 1完全不用手动判断键是否存在。还可以用Counter直接一步到位from collections import Counter count Counter(words) print(count.most_common(2)) # [(apple, 3), (banana, 2)]另一个典型场景是缓存。函数计算完结果先用参数作为键把结果存到字典里下次同样的参数过来直接取不用重新算cache {} def slow_func(n): if n in cache: return cache[n] result n * n # 假装这里计算很慢 cache[n] result return result这种“字典当缓存”的思路在爬虫、算法题、数据分析脚本里非常实用。新手可以先不用掌握复杂的装饰器语法先把这种手动缓存的模式跑熟后面理解functools.lru_cache就轻松了。4. 集合专题去重、成员判断与关系运算三合一集合是三大容器里代码量最少、但面试题里出现频率最高的一个。原因也很简单去重和集合运算在算法题里太常用了而且大多数人只会在字典里用 set根本没把它的性能和运算符优势发挥出来。4.1 集合的真实身世集合的底层就是一张哈希表只不过它只存键、不存值。所以集合的所有特点都能从“哈希表只存键”推导出来元素必须可哈希元素不能重复元素没有可靠的顺序判断成员超快。因为底层是哈希表所以集合里绝对不能放列表这样的可变对象。你要想放多个元素进去只能用元组或者frozenset。创建一个集合s1 {1, 2, 3} # 用花括号 s2 set([1, 2, 3, 3]) # 从列表转换顺便去重结果是 {1, 2, 3} s3 set() # 空集合必须用 set()不能用 {}{}在 Python 里表示空字典不是空集合。这是新手最容易踩的坑没有之一。每次看到有人写s {}想清空集合结果后续add直接报 AttributeError我都想说花括号这个东西在 Python 里默认属于字典。4.2 集合的基本操作增删查s {1, 2, 3} s.add(4) # 添加一个元素已存在则不报错 s.update([5, 6]) # 批量添加参数可以是列表、元组、另一个集合 s.remove(5) # 删除元素不存在会 KeyError s.discard(10) # 删除元素不存在不报错推荐这个 s.pop() # 随机弹出一个元素因为无序所以是随机的 s.clear() # 清空remove和discard的区别经常考remove删除不存在的元素会直接异常discard则默默做无事发生。如果你不确定元素在不在就用discard省掉一层 if 判断。成员判断是集合性能最亮眼的地方nums_set set(range(10000)) 9999 in nums_set # O(1) 9999 in list(range(10000)) # O(n)数据量一大就卡判断一个元素在不在列表里底层是逐个比较判断在不在集合里底层是哈希直接定位。数据量到十万、百万级别时这种差距是肉眼可见的。所以当你需要频繁做成员判断时第一反应应该是把数据转成集合。4.3 集合的关系运算交集、并集、差集与对称差这是集合区别于所有其他容器的核心能力也是算法题里经常出现的一招。a {1, 2, 3, 4} b {3, 4, 5, 6} a b # {3, 4} 交集 a | b # {1, 2, 3, 4, 5, 6} 并集 a - b # {1, 2} 差集在a但不在b b - a # {5, 6} a ^ b # {1, 2, 5, 6} 对称差只在其中一个集合里对应的方法写法是a.intersection(b)、a.union(b)、a.difference(b)、a.symmetric_difference(b)。运算符写起来更短方法名更好读看团队习惯。还有三个判断关系的操作a b # a 是 b 的子集 a b # a 是 b 的真子集 a.isdisjoint(b) # 两个集合是否有交集这里我要特别提醒一个高频坑很多人以为a and b能算交集a or b能算并集这是错的。and和or是逻辑运算符对两个集合操作时返回的是两个操作数之一不是新集合。你写a and b会得到b写a or b会得到a完全不是想要的结果。想用运算老老实实用 | - ^。4.4 去重后如何保持顺序set去重很快但它会打乱原有顺序。如果数据顺序本身是有意义的比如去重后还要保持第一次出现的顺序直接用set()就不行了。有一个很实用的组合拳用dict.fromkeys()去重。因为字典从 Python 3.7 开始保留插入顺序而键又天然不能重复所以data [apple, banana, apple, orange, banana, grape] unique list(dict.fromkeys(data)) # [apple, banana, orange, grape]这个写法既去重又保序比set一步到位但顺序乱掉更符合实际业务需求。缺点是代码不好读最好加一行注释解释为什么这么写不然同事看到会懵。5. 组合实战把三大容器放到一个例子里用单独讲列表、字典、集合都比较简单真正考验人的是混合使用。这里我准备几个完整的代码案例都是平时写脚本、做数据分析、刷算法题时会出现的高频模式。5.1 场景一学生成绩统计假设你有一个原始数据格式是“学生姓名科目分数”要求输出每个学生的总分和平均分以及全场出现次数最多的科目。raw_data [ (张三, 语文, 88), (张三, 数学, 92), (李四, 语文, 79), (李四, 英语, 85), (王五, 数学, 95), (王五, 英语, 91), ] from collections import defaultdict, Counter scores defaultdict(list) # 姓名 - 分数列表 subjects Counter() # 科目 - 出现次数 for name, subject, score in raw_data: scores[name].append(score) subjects[subject] 1 for name, score_list in scores.items(): print(f{name}: 总分 {sum(score_list)}, 平均分 {sum(score_list) / len(score_list):.1f}) print(出现最多的科目:, subjects.most_common(1))这里scores是字典score_list是列表subjects是字典的子类。三种容器配合得很自然几乎看不出衔接痕迹。用defaultdict(list)省去了判断“这个学生第一次出现”的代码用Counter省去了手动统计次数的代码。5.2 场景二两个列表如何快速变成字典爬虫或者处理表格数据时经常遇到两个长度一样的列表一个当键、一个当值。正确姿势是用zip组合names [张三, 李四, 王五] ages [20, 22, 21] name_to_age dict(zip(names, ages)) print(name_to_age) # {张三: 20, 李四: 22, 王五: 21}反过来想把字典拆成两个列表也没问题keys list(name_to_age.keys()) values list(name_to_age.values())还有一个很实用的场景根据条件过滤字典。filtered {k: v for k, v in name_to_age.items() if v 21} # {李四: 22, 王五: 21}字典推导式在数据处理中的出场率很高尤其是从一个大字典里抽出符合条件的子集。5.3 场景三用集合做主数据对比公司有两个系统的用户名单都是列表形式。要快速找出哪些用户两个系统都有哪些只在第一个系统里用集合是最快的。system_a [u001, u002, u003, u004] system_b [u003, u004, u005, u006] set_a set(system_a) set_b set(system_b) print(两个系统都有:, set_a set_b) # {u003, u004} print(只在A系统:, set_a - set_b) # {u001, u002} print(只在B系统:, set_b - set_a) # {u005, u006} print(全部用户:, set_a | set_b) # {u001, u002, u003, u004, u005, u006}如果数据量是在十万行以上这个方案的性能优势非常明显。列表做差集要嵌套循环时间复杂度 O(n*m)集合运算内部是哈希定位接近 O(n m)完全不是一个数量级的体验。5.4 性能对比数据量大的时候选型差异有时间可以用timeit做个粗暴测试比较在大列表里用in和大集合里用in的速度差异。import timeit big_list list(range(1000000)) big_set set(big_list) list_time timeit.timeit(999999 in big_list, globalsglobals(), number100) set_time timeit.timeit(999999 in big_set, globalsglobals(), number100) print(list in 耗时:, list_time) print(set in 耗时:, set_time)我自己实测过在百万级数据里集合的成员判断速度比列表快上几个数量级。不是说列表一无是处而是每种容器都有自己的最佳使用场景要保序、按下标取用列表要高速查找、去重、集合运算用集合要键值映射、分组统计用字典。6. Python 入门常见坑与排查心得最后把这些年在实际项目和答疑群里见过的坑集中聊一遍。这些坑单看都不难但每一个都能让新手卡住十几分钟甚至半天。6.1 为什么字典键不能用列表原因前面提过字典键必须是可哈希的而哈希的前提是不可变。你可以把哈希值理解成对象的“指纹”指纹应该稳定不变。列表可以增删元素对它求哈希值前后可能不一样字典就不知道该把数据放哪了。如果确实需要一个可变的东西当键官方建议改成元组。比如你是想用[x, y]当键就改成(x, y)d {} d[(1, 2)] 坐标点6.2 遍历列表时为什么删不干净新手写去重经常这样lst [1, 2, 3, 2, 4, 2] for x in lst: if x 2: lst.remove(x)结果遍历完列表里还残留一个 2。原因是遍历过程是按下标进行的删掉元素后后面的元素会补位for 循环却依然按原下标继续走于是跳过了补位上来的元素。正确的做法有几种最简单的是用列表推导式生成新列表lst [x for x in lst if x ! 2]如果一定要在原列表上操作就倒序遍历for i in range(len(lst) - 1, -1, -1): if lst[i] 2: lst.pop(i)这个原则在字典和集合里同样适用你一边遍历一边修改容器很容易触发RuntimeError: dictionary changed size during iteration。需要修改就先收集要删的键遍历结束后再统一删。6.3 列表赋值是复制还是共享这是面试必问题实际开发也容易踩b a不会复制列表它只是让b和a指向同一个列表对象。改b就是改a。a [1, 2, 3] b a b.append(4) print(a) # [1, 2, 3, 4]想复制列表可以用切片b a[:]或者b a.copy()。但这两个都是浅拷贝如果列表里还有列表内层列表依然是共享的a [[1, 2], [3, 4]] b a.copy() b[0][0] 99 print(a) # [[99, 2], [3, 4]]要彻底独立复制得用copy.deepcopy()。判断一句你的数据是只有一层还是嵌套多层只有一层用浅拷贝多层用深拷贝。6.4 用 and/or 做集合运算引发的幻觉集合运算必须用、|、-、^不要用and、or。and和or是逻辑运算符对两个集合求值时返回的是其中一个操作数不是逻辑上的“交集”或“并集”。如果你发现集合运算结果不对先看是不是用了and。这是搜索热词里“集合反转”“3个元素的集合有多少拓扑”那一类问题之外最容易让人困惑的集合知识点了。6.5 选列表还是选元组我的建议很简单如果数据是“会变化的集合”比如待处理的任务列表、动态计算结果用列表如果数据是“固定配置”比如坐标、RGB 颜色值、函数返回值用元组。过度使用元组会让代码不好维护因为无法增删确实不方便过度使用列表则容易写出隐藏 bug因为某个模块可能会无意间修改共享数据。这里没有绝对标准实际经验多了自然会有手感。说到最后容器这块我一直建议新手不要死记 API而是把“解决什么场景的问题”记清楚。看到“按位置存一堆同类型数据”想列表看到“名字查值、分组统计”想字典看到“去重、快速判断、集合关系”想集合。数据结构选对了代码自然清晰一半。我再分享一个小习惯每次写完一段容器操作随手打印一下type()和len()能非常快地发现变量类型是不是自己以为的那一个。很多报错其实不是逻辑错了是列表和元组、字典和集合之间一不小心搞混了。这种基本功越早越扎实越好。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。