资讯详情

资讯详情

Python数据结构入门:列表、元组、集合、字典的选择与实战

1. 为什么说数据结构是Python里的“智能容器”刚接触Python的人十有八九会经历这样一个阶段变量学完了if和for也能写几行可一旦要处理一批数据比如统计全班同学的成绩、记录一周的天气、管理一个通讯录立刻就卡住了。问题不在于语法而在于脑子里没有“容器”这个概念。单个变量就像手里只能拿一颗糖而数据结构是让你拥有一个糖罐、一个抽屉柜、一个自动分类的收纳盒。我常跟刚入门的朋友打一个比方变量是散落在地上的乐高积木数据结构就是按颜色、形状分好的收纳格。你当然可以把所有积木堆成一堆但想找一块红色的2x4时就得翻半天。Python内置的四种核心数据结构——列表List、元组Tuple、集合Set、字典Dictionary——就是四种不同规则的收纳格。它们各自解决不同的问题选错了不会报错但会让你的代码又慢又乱。这篇内容适合谁如果你已经会写Python的基本语句但对“什么时候用列表、什么时候用字典”还靠感觉或者你写出来的代码总是用一个大列表装所有东西、查个数据要循环半天那这篇就是为你准备的。我会把这四种结构拆开揉碎讲清楚它们底层怎么存数据、什么场景该用谁、新手最容易踩的坑在哪以及一些能直接抄去用的实操套路。全程不堆术语用生活场景和可运行的代码说话。先给一个全局的认知框架这是很多人学完四种结构后仍然模糊的地方结构有序性可变性元素唯一性查找方式一句话定位列表 List有序可变可重复按位置索引排队买票谁先来谁在前元组 Tuple有序不可变可重复按位置索引刻在石头上的名单改不了集合 Set无序可变唯一按值判断存在抽奖箱重复的球自动去掉字典 Dict有序3.7可变键唯一按键查找通讯录名字直接翻到电话这张表建议你先记住“有序性”和“可变性”两列因为后面所有的选择逻辑几乎都从这两点推导出来。接下来我们逐个拆解但不会按教科书顺序平铺而是从“你实际会遇到的问题”切入。2. 列表最顺手但也最容易被滥用的那个2.1 列表的本质是一排带编号的格子列表是绝大多数人第一个学会的数据结构因为它最直观。scores [88, 92, 75, 96]你脑子里浮现的就是一排格子每个格子有编号从0开始。这个“编号”就是索引scores[0]拿到88scores[-1]拿到最后一个96。负索引这个设计非常贴心取末尾元素不用先算长度。但新手往往忽略一件事列表的索引是“位置”不是“身份”。这意味着如果你在列表中间插入或删除一个元素后面所有元素的索引都会变。我见过太多人写出这样的代码先记住某个重要数据在索引3然后往前面插了一条数据再去data[3]取结果取到了错误的东西。这不是bug是你对“位置会移动”这件事没有警觉。tasks [写报告, 回邮件, 开会] important tasks[1] # 回邮件 tasks.insert(0, 紧急修复) print(tasks[1]) # 现在是写报告不是回邮件了所以我的第一条实操建议是如果一个数据需要被反复引用不要靠索引记住它要么用变量存下来要么考虑用字典给它一个稳定的“名字”。索引只适合“我就是要按顺序处理每一个”的场景。2.2 增删改查的常用手法与性能直觉列表的操作方法很多append、insert、remove、pop、extend、sort……新手容易混。我按“从尾部操作”和“从中间操作”分两类因为它们的性能差异巨大。从尾部操作append、pop()不带参数非常快可以理解为O(1)不管列表多长耗时基本恒定。从头部或中间操作insert(0, x)、pop(0)、remove(x)就慢了因为后面所有元素都要挪位置是O(n)。如果你发现自己频繁往列表头部插数据那说明列表选错了应该用collections.deque这是后话但意识要有。# 推荐尾部追加 results [] for i in range(1000): results.append(i * 2) # 不推荐头部插入 results [] for i in range(1000): results.insert(0, i * 2) # 每次都挪动全部元素sort()和sorted()的区别也是高频困惑点。sort()是原地排序直接改列表本身返回Nonesorted()是返回一个新列表原列表不动。新手常犯的错是new old.sort()结果new是None。记住一句话带ed的通常返回新对象不带ed的通常原地修改这个规律在Python里反复出现。2.3 列表推导式让循环变得优雅的利器当你需要“从一个列表生成另一个列表”时列表推导式List Comprehension是必须掌握的。它不只是语法糖写多了之后你会发现它强迫你把“筛选条件”和“转换逻辑”想清楚。# 传统写法 squares [] for x in range(10): if x % 2 0: squares.append(x ** 2) # 推导式写法 squares [x ** 2 for x in range(10) if x % 2 0]但我要提醒一个坑推导式里不要塞太复杂的逻辑。如果你发现推导式里嵌套了三层for加两个if那它已经失去了可读性优势老老实实写循环更好。我个人的经验法则是推导式超过一行80个字符就考虑拆开。还有一个隐蔽的坑是变量泄漏。在Python 3里推导式的循环变量不会泄漏到外部这是好事。但如果你在推导式里引用了外部变量要小心作用域的理解。这些细节新手阶段不用深究但知道“推导式有自己的小作用域”能帮你避免一些莫名其妙的错误。3. 元组那个“不能改”的特性到底有什么用3.1 不可变不是限制而是一种承诺很多新手学元组时只有一个印象“哦就是不能改的列表。”然后就想那我干嘛不直接用列表这个疑问非常正常因为如果元组只是“不能改”它确实显得多余。但关键在于不可变意味着可哈希、可预测、可安全共享。举个实际场景你要把坐标点作为字典的键。point (3, 5)可以point [3, 5]不行因为列表不可哈希。为什么列表不可哈希因为哈希要求对象在生命周期内不变否则你把它存进字典后一改就再也找不到了。元组用“不能改”换来了“可以作为键、可以放进集合”的能力。# 元组可以作为字典的键 locations { (39.9, 116.4): 某地点A, (31.2, 121.5): 某地点B } # 列表不行会报TypeError # locations {[39.9, 116.4]: 某地点A} # 报错另一个场景是函数返回多个值。Python里return x, y本质上返回的就是一个元组。你解包的时候a, b func()其实是在解包元组。这个机制之所以安全就是因为元组不可变不用担心函数返回后被人偷偷改了。3.2 元组解包与星号表达式元组解包是Python里非常优雅的特性值得单独拿出来说。基础用法大家都懂但星号*的用法很多人不熟。first, *middle, last [1, 2, 3, 4, 5] # first1, middle[2,3,4], last5 # 交换变量 a, b b, a # 右边其实是个元组这个*middle在函数传参时也很有用。比如你有一个列表想把它拆成多个参数传给函数用func(*my_list)。反过来函数定义时用def func(*args)接收任意数量的位置参数args就是一个元组。注意是元组不是列表这也体现了“参数一旦接收就不该被修改”的设计意图。3.3 什么时候该用元组而不是列表我给一个简单的判断标准如果这组数据在逻辑上是一个“整体”且每个位置的元素含义固定用元组如果是一批同类数据的集合且会增删用列表。比如表示一个学生的基本信息(张三, 18, 男)这三个位置分别代表姓名、年龄、性别顺序和含义是固定的用元组合适。而一个班级的学生名单[张三, 李四, 王五]随时可能加人减人用列表合适。还有一个性能上的小优势元组比列表占用内存更小创建速度也更快。在数据量极大且不需要修改的场景用元组能省一些资源。不过新手阶段不用为了这点性能刻意优化可读性优先。4. 集合去重和成员判断的隐藏高手4.1 集合的核心价值是“存在性判断”集合最直观的特点是“自动去重”。你把一个有重复的列表转成集合重复项就没了。但集合真正的杀手锏是成员判断的速度。判断一个元素在不在集合里平均是O(1)而在列表里是O(n)。这个差异在小数据量下感觉不到但数据量一大就是天壤之别。import time big_list list(range(1000000)) big_set set(big_list) # 列表判断 start time.time() print(999999 in big_list) print(列表耗时:, time.time() - start) # 集合判断 start time.time() print(999999 in big_set) print(集合耗时:, time.time() - start)跑一下你会发现列表判断可能需要几毫秒到几十毫秒集合判断几乎是瞬间。如果你在循环里反复做in判断比如“检查用户输入是否在允许的名单里”用集合和用列表的差距会随着循环次数放大到无法忽视。4.2 集合运算交并差的实际用途集合支持数学上的交、并、差运算这在处理“标签”“权限”“共同好友”这类问题时特别好用。user_a_tags {科技, 阅读, 旅行} user_b_tags {旅行, 美食, 科技} # 共同兴趣 common user_a_tags user_b_tags # {科技, 旅行} # 所有兴趣 all_tags user_a_tags | user_b_tags # A有但B没有的 only_a user_a_tags - user_b_tags # {阅读} # 对称差只在一个里出现的 diff user_a_tags ^ user_b_tags # {阅读, 美食}这些运算符比写循环加if判断清晰得多。我处理用户标签匹配时第一反应就是用集合运算代码短且不容易出错。4.3 集合的坑无序性和不可索引集合是无序的这意味着你不能用my_set[0]取元素因为“第0个”这个概念不存在。如果你需要顺序就得转成列表再排序。另外集合里只能放不可变对象列表不能放进集合元组可以。还有一个新手常踩的坑{}创建的是空字典不是空集合。空集合要用set()。empty_dict {} # 这是字典 empty_set set() # 这才是空集合这个设计确实有点反直觉但记住就好。集合用{}表示非空的时候没问题{1, 2, 3}是集合只有空的时候有歧义。5. 字典用“名字”而不是“位置”找数据5.1 字典的键值对思维是编程的重要跃迁从列表到字典是新手思维的一次重要升级从“第几个”变成“叫什么”。列表靠位置索引字典靠键查找。这个转变的意义在于你的数据组织方式开始贴近现实世界的命名逻辑。# 列表方式靠位置容易记错 student [张三, 18, 男, 92] # 字典方式靠名字自解释 student { name: 张三, age: 18, gender: 男, score: 92 }字典方式明显更清晰student[score]一眼就知道在取成绩而student[3]需要你记住第3位是成绩。代码是给人看的字典在可读性上的优势巨大。5.2 键的不可变性要求与哈希原理字典的键必须是不可变对象原因和前面说的哈希有关。字典底层用哈希表实现键的哈希值决定了它存在哪个“桶”里。如果键能改哈希值就变了下次查找就找不到原来的位置了。所以列表不能做键元组可以字符串和数字更可以。# 合法 d {name: 张三, 1: one, (1, 2): 坐标} # 非法 # d {[1, 2]: 坐标} # TypeError: unhashable type: list这里有个实用技巧如果你确实需要用一组数据做键把它转成元组。比如用(x, y)坐标做键而不是[x, y]。5.3 遍历字典的三种姿势与选择遍历字典有keys()、values()、items()三种方式。新手容易写成for key in d:然后d[key]取值其实直接for key, value in d.items():更高效也更清晰。scores {语文: 92, 数学: 88, 英语: 95} # 只遍历键 for subject in scores: print(subject) # 只遍历值 for score in scores.values(): print(score) # 同时遍历键值推荐 for subject, score in scores.items(): print(f{subject}: {score})items()返回的是键值对元组解包后直接用。在Python 3.7之后字典保持插入顺序所以遍历顺序是可预测的这一点比早期版本友好很多。5.4 字典的常用操作与默认值处理get()方法是字典里最值得养成的习惯之一。d[不存在的键]会直接报KeyError而d.get(不存在的键, 默认值)会返回默认值不会崩。counts {} words [apple, banana, apple, cherry, banana, apple] # 传统写法 for w in words: if w in counts: counts[w] 1 else: counts[w] 1 # get写法 for w in words: counts[w] counts.get(w, 0) 1统计词频这个例子非常经典get(w, 0) 1这个套路值得背下来。更进一步可以用collections.defaultdict或collections.Counter但那是进阶内容新手先把get用熟。6. 四种结构的选择逻辑与组合使用6.1 一张决策流程图帮你选对结构面对一个具体问题怎么快速决定用哪种结构我总结了一个简单的决策顺序需要键值对应关系吗需要就用字典。比如“学号→成绩”“用户名→密码”。需要去重或快速判断存在吗需要就用集合。比如“统计出现了哪些单词”“检查是否已访问”。数据会频繁增删吗会就用列表。比如“待办事项”“消息队列”。数据是固定不变的记录吗是就用元组。比如“坐标”“配置项”“函数返回多值”。这个顺序不是绝对的但能覆盖大部分场景。实际开发中这四种结构经常嵌套使用比如“字典的值是列表”“列表里装字典”“集合里放元组”。# 列表装字典多个学生记录 students [ {name: 张三, score: 92}, {name: 李四, score: 88} ] # 字典的值是列表每个科目的多个成绩 grades { 语文: [92, 85, 78], 数学: [88, 90, 95] } # 字典的值是集合每个用户的标签 user_tags { user1: {科技, 阅读}, user2: {旅行, 美食} }6.2 嵌套结构的访问与修改嵌套结构的访问要一层层剥开新手容易在“到底哪层是列表哪层是字典”上绕晕。我的建议是写代码时把结构画出来或者用注释标清楚。# 访问第一个学生的名字 students[0][name] # 给语文加一个成绩 grades[语文].append(99) # 给user1加标签 user_tags[user1].add(运动)修改嵌套结构时要特别注意“可变对象是引用”这件事。如果你把一个列表赋给两个变量改一个另一个也会变因为它们指向同一个对象。a [1, 2, 3] b a b.append(4) print(a) # [1, 2, 3, 4]a也变了 # 想要独立副本 c a.copy() # 浅拷贝 c.append(5) print(a) # [1, 2, 3, 4]a没变浅拷贝只复制第一层如果列表里还有列表内层还是共享的。需要完全独立就用copy.deepcopy()。这个坑在处理嵌套数据时非常常见我踩过不止一次。6.3 常见误用与性能陷阱最后列几个我见过最多的误用帮你提前避开用列表做成员判断数据量大时换成集合速度差几十倍。用列表存键值对再循环查找直接上字典别自己造轮子。频繁在列表头部插入删除用deque或反过来从尾部操作。用可变对象做字典键会直接报错用元组代替列表。忘记字典的键是唯一的后赋的值会覆盖前面的需要多值就用列表做值。# 错误想存多个值结果被覆盖 d {} d[key] 1 d[key] 2 print(d) # {key: 2}1没了 # 正确值用列表 d {} d.setdefault(key, []).append(1) d.setdefault(key, []).append(2) print(d) # {key: [1, 2]}setdefault这个方法是处理“一键多值”的利器比先判断再初始化简洁。不过它每次都会创建默认值即使键已存在在极端性能场景下要注意一般用defaultdict更合适。7. 从新手到熟练几个能立刻用上的实操习惯学完四种结构真正的差距不在“知不知道”而在“用不用得对”。我分享几个自己长期坚持的习惯都是踩坑后总结出来的。第一个习惯写代码前先问自己“这个数据的生命周期是什么”。如果它只在一次循环里用可能不需要容器如果它要跨函数传递考虑用元组保证不被改如果它要频繁查询字典或集合优先。这个思考只要几秒钟但能避免大量返工。第二个习惯给变量起能体现结构类型的名字。student_list、score_dict、visited_set、point_tuple虽然有点啰嗦但在复杂代码里一眼就能看出该用什么操作。我见过太多data、temp、result满天飞的代码读起来像解密。第三个习惯善用len()、in、for这三个通用操作。它们对四种结构都适用写代码时先用这三个把逻辑跑通再根据性能需要替换成更专门的方法。比如先用列表加in判断跑通发现慢了再换集合。第四个习惯遇到KeyError、IndexError先别急着加try想想是不是结构选错了。字典报KeyError往往是因为你该用get列表报IndexError往往是因为你该用字典或先判断长度。错误是信号不是敌人。最后一个心得把这四种结构想象成四种工具而不是四个知识点。锤子、螺丝刀、扳手、钳子你不会问“哪个最好”只会问“现在要拧什么”。列表适合顺序处理元组适合固定记录集合适合去重判断字典适合快速查找。用多了之后选择会变成直觉。如果非要给一个练习建议我会说找一段你以前用列表硬写的代码看看能不能拆成字典加列表的组合找一个需要去重的脚本把列表换成集合找一个返回多个值的函数确认它返回的是元组并正确解包。改上三五次这四种结构就真正长在你手上了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →