ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python字典、深浅拷贝与列表推导式:从原理到实战

2026/9/11 15:56:40 拓冰建站 浏览量
Python字典、深浅拷贝与列表推导式:从原理到实战 先交代一个背景我这些年带过不少Python新人发现大家卡壳的点其实高度集中排在最前面的就是字典、深浅拷贝、列表推导式这三样。不是它们有多难而是市面上的教程要么把每个知识点拆得太碎要么直接扔一段代码让你背学完还是一头雾水。这篇文章就围绕这三个核心知识点来写从零基础视角出发讲到进阶用法把原理、实操、坑位一次讲透。适合刚学完Python基础语法、准备写真实项目的读者也适合学了一段时间但总觉得字典用得不够6、拷贝老是搞混、推导式只会照抄的朋友。1. 上手准备先把环境理清楚1.1 一套零成本的学习环境学习Python的第一步不是背语法而是把环境装好。这里直接给你一套最省事的方案Python解释器去Python官网下载最新的稳定版3.10以上都可以3.12系列也没问题。编辑器首推VSCode免费、插件生态好。装好之后记得装Python扩展然后在命令面板里选一下解释器路径。验证方式打开终端输入python --version能输出版本号就说明装好了。很多人会在环境配置上卡很久但说句实话这个环节不值得花费太多时间。装好了能跑就行等以后写大项目了再研究虚拟环境、包管理那些东西。1.2 为什么先学这三个知识点字典、深浅拷贝、列表推导式这三个点放在一起学其实有一条隐藏的逻辑线字典是Python里最常用的数据结构之一存配置、传参数、处理JSON处处都有它。深浅拷贝是理解Python变量本质的分水岭搞不懂它后面学函数传参、类对象、数据处理一定踩坑。列表推导式是写出Pythonic代码的第一道门槛也是从写循环到表达数据变换的思维升级。所以这篇内容的路线就是先学会字典怎么存数据再弄明白变量之间怎么复制数据最后学会用一行代码高效地变换数据。这三板斧下来你已经具备读别人代码和写小工具的基本能力了。2. 字典Python里的电话簿但灵活得多2.1 从生活场景理解字典字典dict在Python里是一种键值对key-value的映射结构。如果拿电话簿来类比键就是联系人姓名值就是电话号码。你想找谁的电话直接用名字查就行不需要从头翻到尾。为什么Python要专门设计一个字典因为这种按键查找的速度非常快。列表要一个个遍历才能找到目标而字典内部通过哈希表实现平均情况下查找时间复杂度是O(1)——意思是数据量再大查一次也基本是瞬间完成。创建字典有几种常见方式# 方式一花括号 student {name: 张三, score: 92} # 方式二dict() 构造函数 student2 dict(name李四, score88) # 方式三从键值对序列创建 pairs [(name, 王五), (score, 76)] student3 dict(pairs) # 方式四字典推导式后面会细讲 numbers {i: i ** 2 for i in range(5)}这里有个细节字典的键必须是可哈希hashable的。简单说键得是不可变类型比如字符串、数字、元组而列表、字典这种可变类型不能当键。原因也很容易理解——可变对象的值会变哈希值也跟着变那之前存进去的数据就找不到了。2.2 日常操作增删改查的边界在哪里字典的基本操作并不难但有几个细节很容易忽略student {name: 张三, score: 92} # 增 / 改key不存在就是新增存在就是更新 student[age] 20 student[score] 95 # 查直接用中括号但key不存在会报KeyError print(student[name]) # 更安全的查法用get不存在时返回None或你给的默认值 print(student.get(gender)) # None print(student.get(gender, 男)) # 男 # 删除pop会返回被删的值del直接删 age student.pop(age) del student[score]新手最常犯的错误就是不管三七二十一用student[key]去取值结果key不存在直接崩溃。实际业务里数据来源往往不可控所以养成用get()的习惯是第一步。还有一点值得注意字典在Python 3.7之后保证插入顺序也就是说你按什么顺序塞进去的遍历时就是什么顺序。但这不代表字典就应该被当成有序列表用——它的设计初衷还是快速按键查值顺序只是顺带的保证。2.3 三个让字典更好用的进阶技巧基础操作掌握之后下面这几个技巧能明显减少你的代码量第一个技巧setdefault处理键不存在就初始化的场景。比如你要统计一段文字里每个字符出现的次数text hello python counter {} for ch in text: counter[ch] counter.get(ch, 0) 1用get已经很简洁了但还有更优雅的方式counter {} for ch in text: counter.setdefault(ch, 0) counter[ch] 1setdefault(key, default)的意思是如果key存在就返回它的值如果不存在就把key设为default并返回default。很多老手更喜欢用collections.defaultdict效果类似但在某些场景下setdefault更直观。第二个技巧用字典解构处理嵌套数据。实际开发中字典往往不是一层的。比如从接口返回的学生数据可能是这样的data { student: { name: 张三, info: {age: 20, city: 北京} } } name data[student][name] city data[student][info][city]这里的教训是每多一层嵌套取值时就要多写一层中括号代码越来越丑还容易因为某层缺key直接崩溃。更稳的写法是配合get层层防御info data.get(student, {}).get(info, {}) city info.get(city, 未知)第三个技巧两个字典合并。Python 3.9之后字典支持了|运算符base {name: 张三, score: 92} extra {score: 98, gender: 男} merged base | extra print(merged) # {name: 张三, score: 98, gender: 男}注意优先级后面的字典会覆盖前面同名key的值。在3.9之前通用的写法是{**base, **extra}效果一样。这个技巧在合并配置项时特别实用。提示字典是可变的但它的键必须是不可变类型。这个约束不是Python故意刁难你而是哈希表的基本原理决定的理解这一点很多为什么就通了。3. 深拷贝与浅拷贝变量的分身术3.1 先说透赋值、浅拷贝、深拷贝的差别很多教程一上来就扔概念说浅拷贝只复制一层深拷贝递归复制全部听得人一头雾水。我换个说法来讲。假设你有一份名单名单上每一行写着一个学生的信息。现在你面临三种情况直接赋值相当于给这份名单多贴了一个标签改标签A上的内容标签B看到的也跟着变。因为本质上是同一份名单。浅拷贝你另外拿了一个新本子把名单上每一行照抄了一遍。注意你抄的是每一行写着的内容如果某个格子里的信息本身是另一张纸条嵌套的可变对象那抄的还是那张纸条的引用。深拷贝不是照抄而是把名单上每行内容、包括格子里夹着的纸条全部重新复制一份。新本子和旧本子彻底没关系。用代码来验证import copy original {student: [张三, 92], grade: A} # 直接赋值 ref original # 浅拷贝 shallow original.copy() # 深拷贝 deep copy.deepcopy(original) # 修改原始数据的嵌套列表 original[student][0] 李四 print(ref[student][0]) # 李四赋值跟着变 print(shallow[student][0]) # 李四浅拷贝的嵌套层也跟着变 print(deep[student][0]) # 张三深拷贝完全隔离这就是经典的三者区别。新手最容易困惑的是明明用了.copy()为什么内部数据还是被改了因为浅拷贝只复制了外层容器容器里面的元素如果本身是可变对象复制过去的还是引用。3.2 拷贝背后的内存原理想要彻底理解拷贝得稍微提一下Python对象在内存里的模型。当你写a [1, 2]时其实是创建了一个列表对象然后a这个变量名指向它。再写b a并没有创建新列表只是让b也指向同一个对象。所以b.append(3)a当然也变。浅拷贝list.copy()、dict.copy()、copy.copy()会创建一个新的容器对象然后往里面塞原容器里每个元素的引用。如果原容器里的元素是不可变类型数字、字符串那完全没问题因为不可变对象本身就不会被修改但如果元素是列表、字典这种可变对象引用就出问题了——你通过任意一个容器修改了这个可变对象另一边也会看到变化。深拷贝则完全递归地走一遍把所有能拷贝的对象都创建一份新的直到底层全是不可变对象为止。下面这张表帮你快速对照操作是否创建新容器内部可变对象是否复制修改嵌套数据是否互相影响直接赋值否否是浅拷贝是否仍是引用是深拷贝是是全部复制否3.3 哪些场景必须用深拷贝哪些用浅拷贝就够了很多人以为深拷贝比浅拷贝高级所以处处用深拷贝。这其实是误解深拷贝有成本而且有些对象根本没法深拷贝。必须用深拷贝的场景你有一份配置字典程序运行时要基于它做很多临时修改但不允许改动原始配置。你在做数据处理需要生成一份快照后续操作都基于快照不能影响源数据。写递归算法时需要把某个嵌套结构传下去并在不同分支中独立修改。用浅拷贝就够的场景只复制一层数据内部没有可变对象。明确知道自己要共享内层对象只为外层套一个新壳。举个例子如果你有一个列表里面全是数字和字符串那浅拷贝和深拷贝效果一样用浅拷贝就够了还省性能nums [1, 2, 3] copy_nums nums.copy() # 足够安全因为元素全是不可变类型还有一点要提醒不要用json.loads(json.dumps(data))来模拟深拷贝。虽然对纯JSON数据它能work但遇到元组、自定义对象、非JSON类型就会出错而且性能比copy.deepcopy差。用pickle做深拷贝也不推荐一样有类型限制和安全隐患。注意深拷贝不是万能的如果对象里包含文件句柄、数据库连接、线程锁这类资源型对象deepcopy会直接报错或者拷出一个没有意义的东西。这些场景下你需要手动管理复制逻辑。4. 列表推导式一行代码干掉三行循环4.1 从翻译开始理解推导式列表推导式List Comprehension用一句话概括用表达式来描述我要一个什么样的列表。它不是什么高深魔法就是Python提供的一种更紧凑的列表构建方式。先看一个最朴素的例子生成0到9的平方。普通写法squares [] for i in range(10): squares.append(i ** 2)列表推导式写法squares [i ** 2 for i in range(10)]如果你觉得第二种写法看不明白可以心里默默做一次翻译[i ** 2 for i in range(10)]拆开来看for i in range(10)是循环来源写在for前面的i ** 2是每一轮循环要把什么东西放进列表。所以结构就是[ 表达式 for 变量 in 可迭代对象 ]这个模式下你每次循环都往新列表里塞一个表达式的值。推导式的价值不仅是省代码更在于它把创建空列表、循环、append这三步合并成了声明式的写法读起来一目了然。4.2 条件过滤与多层循环推导式还支持在尾部加if条件用来过滤元素# 只保留偶数 evens [i for i in range(20) if i % 2 0] # 只保留长度大于3的单词 words [cat, dog, python, java] long_words [w.upper() for w in words if len(w) 3]翻译一下先执行for循环再判断if条件条件成立才把表达式的结果放入列表。这是最常用的组合。推导式还能写多层循环相当于嵌套for循环的展开# 两层循环 pairs [(x, y) for x in range(3) for y in range(3)] # 等价于 # pairs [] # for x in range(3): # for y in range(3): # pairs.append((x, y))这里我强烈建议两层以内可以写推导式再多就别写了。我见过有人写个三层嵌套推导式代码缩成一坨bug找半天最后乖乖拆回普通循环。推导式的目的是提高可读性不是制造代码谜语。4.3 列表推导式与生成器表达式的区别热词里把列表推导式与生成器放在一起确实是因为它们长得像。区别就在括号# 列表推导式方括号立即生成完整列表 squares_list [i ** 2 for i in range(10)] # 生成器表达式圆括号惰性生成 squares_gen (i ** 2 for i in range(10))生成器表达式不会一次性把10个元素算出来而是你迭代到哪才算到哪。如果你要处理的是几百万个数据列表推导式会占大量内存生成器表达式就从容得多。核心取舍维度列表推导式生成器表达式语法方括号圆括号执行时机立即计算惰性计算内存占用高一次全存低逐个产出可重复迭代是否用完了就没了适合场景数据量小需要多次访问数据量大只需遍历一次一个很经典的例子是求前N个数的和total sum(i ** 2 for i in range(1000000))这里完全没有必要生成一个百万元素的列表直接用生成器表达式传给sum就行内存占用低到可以忽略。提示推导式是表达式不是语句所以里面不能再写带冒号的复杂逻辑。如果你发现推导式里塞了if...else...还觉得绕说明这个场景更适合写普通循环不必强行用推导式。5. 综合实战用学生成绩系统串起三个知识点5.1 需求描述与数据结构设计前面都是单个知识点的拆解现在把它们组合起来做一个具体的小项目你才能体会这三个知识点在一段真实代码里是怎么配合的。需求很简单有一个班级的学生成绩数据每个学生有姓名、班级、平时成绩、期末成绩。需要实现以下功能按姓名查学生的综合成绩综合成绩 平时成绩 * 30% 期末成绩 * 70%。统计所有期末成绩高于90分的学生姓名并统一格式化成大写。将原始数据做一份隔离副本后续模拟成绩复核时不影响原始数据。先设计数据结构。每个学生用字典表示整个班级用列表存多个字典students [ {name: zhang san, class: A, usual: 88, final: 92}, {name: li si, class: A, usual: 76, final: 85}, {name: wang wu, class: B, usual: 95, final: 89}, {name: zhao liu, class: B, usual: 60, final: 78}, ]5.2 字典查值与列表推导式统计按姓名查综合成绩最直接的想法是遍历列表判断字典里的name字段。但既然学了字典就应该明白按键查找更快更优雅。所以可以先把学生列表转成一个以姓名为键的字典students_by_name {s[name]: s for s in students}这就是字典推导式的应用。一行代码把列表变成了姓名索引之后查询就是O(1)。def query_score(name): student students_by_name.get(name) if not student: return None return student[usual] * 0.3 student[final] * 0.7 print(query_score(zhang san)) # 90.8再统计期末考试高于90分的学生并且把姓名转大写top_students [s[name].upper() for s in students if s[final] 90] print(top_students) # [ZHANG SAN]这里的两个知识点配合得很自然列表推导式负责遍历过滤变换字典负责构建查询索引。5.3 用深拷贝做草稿与终稿隔离现在模拟成绩复核流程。假设复核人员在副本上修改成绩但不能动原始数据。import copy # 如果不做深拷贝直接赋值 draft students draft[0][final] 60 print(students[0][final]) # 60源头被改了 # 正确做法深拷贝 draft copy.deepcopy(students) draft[0][final] 60 print(students[0][final]) # 92原始数据不受影响为什么这里不能用浅拷贝因为students列表里的每个元素都是字典字典是可变对象。浅拷贝只复制了外层列表列表里的字典仍然是原对象的引用。所以修改draft[0][final]时实际上改的是同一个字典原始数据自然被污染了。你可以在自己电脑上跑一遍这段代码把深拷贝改成students.copy()对比一下输出结果这个知识点立刻就能刻进脑子里。5.4 实战中的性能与可读性思考这个综合案例虽然小但已经能体现真实项目的影子读原始数据、建立索引、筛选变换、防数据污染。在性能层面把列表转成字典的过程是一次O(n)的预处理之后每次查询都是O(1)。如果查询次数非常多这个预处理就非常划算如果只查一次直接用for循环遍历反而简单。在可读性层面用students_by_name.get(name)比写一个for循环判断字段清晰得多这也是字典相比列表的核心价值。这其实引出一个更高层的经验数据结构选型比算法技巧更重要。数据是列表还是字典决定了你后面写代码的舒服程度和运行效率。6. 常见问题与排查技巧实录6.1 新手高频报错速查表我在带新人的过程中发现下面几个报错出现频率最高。这里整理成速查表你遇到类似问题直接对照排查报错信息出错的场景排查方向KeyError: xxx字典取值时key不存在改用dict.get(key, default)或先判断key in dictTypeError: unhashable type: list用列表当字典的键换成元组或其他不可变类型TypeError: int object is not iterable对数字用for...in...检查是不是误把整数当成了可迭代对象RecursionErrorcopy.deepcopy一个包含自我引用的对象检查数据结构里是否存在循环引用考虑手动处理复制逻辑AttributeError: NoneType object has no attribute ...函数返回了None却继续调用方法检查get()是否返回了默认值None后续没有防空判断前三个是语法和数据结构层面的问题新手遇到最多后两个是深拷贝和函数返回值的经典坑已经写进很多公司面试题里了。6.2 我踩过的几个坑与避坑经验第一个坑在循环里修改正在遍历的字典。data {a: 1, b: 2, c: 3} for key in data: if key b: del data[key] # 运行时崩溃dictionary changed size during iteration正确做法是收集要删除的key循环结束后再删或者直接构造新字典data {k: v for k, v in data.items() if k ! b}这个坑的根源在于字典迭代器对容器长度敏感遍历途中改变大小直接报错。第二个坑用copy.copy去复制嵌套结构结果改了一处另一处也变。这个在前面已经演示过不再赘述。记住一句话你copy的是不可变元素占主导的结构浅拷贝放心用一旦涉及嵌套的可变对象先想想深拷贝。第三个坑列表推导式写得太长把自己绕晕。我见过最夸张的是一行写了十几个单词、两层循环加两个条件最后维护的人头疼欲裂。后来团队干脆约定推导式超过一行约80个字符就拆成普通循环或者分段写成多个推导式。第四个坑把字典当JSON直接传。字典虽然和JSON结构很像但JSON要求键必须是字符串而Python字典的键可以是数字、元组等。所以往接口传数据之前用json.dumps序列化时如果报错先回头检查字典的键类型。6.3 学习路径上的一个建议写到这里我想额外说一句这三个知识点不是看过就完事的你想要真正掌握一定要在代码里反复用。我自己的经验是每学一个新知识点就强制自己在接下来的三个小项目里至少用一次。比如想巩固列表推导式那就把所有创建列表并填充的代码全部改成推导式写法想巩固深拷贝就在每次处理嵌套字典时主动问一句这里会不会污染原始数据。这种强制使用的方法看起来很笨但实际上是最有效的内化方式。语法看十遍不如自己敲一遍敲一遍不如在真实场景里踩一个坑再爬起来。如果你正在自学Python建议把这三个知识点的代码全部手敲一遍然后自己给自己出几个小题目比如生成一个九九乘法表、统计一段英文文章的词频、实现一个简单的配置模板替换功能。做完这些你不仅能玩转字典、拷贝和推导式还会对Python的数据处理思维上一个台阶。