2026 Python 数据类型进阶:list/tuple/dict/set、引用传递与深浅拷贝全解析
免费基金定投助手全功能拆解:为什么你的基金定投还在亏钱?因为你的工具用错了。动态平衡仓位管理+8种智能定投策略引擎,会自己算买卖点的定投系统-CSDN博客
https://download.csdn.net/download/weitingfu/93448039?spm=1001.2014.3001.5503
开篇黄金 100 字
你是不是遇到过这种情况:明明只是b = a,结果改b[0]时a也跟着变了?或者听说 tuple 不可变,却搞不清它和 list 到底差在哪?这两个问题的答案都指向同一个核心概念——可变性与引用。本篇把 list/tuple/dict/set 四种容器一次讲透,再用深浅拷贝的内存图把"引用传递"钉死在脑子里,帮你把面试基础分拿满。
前置知识说明
本篇要求你已掌握 P03 的 Python 基础语法(变量、控制流、函数、类),知道for item in lst遍历和len()取长度即可。如果你还没完全熟练,也可以直接读——本篇所有代码都很短,重点在"现象解释"而非语法细节。建议准备好 Python 环境,边读边敲。
本篇地图
| 模块 | 核心问题 | 预计时间 |
|---|---|---|
| list | 动态数组怎么增删改查? | 15 分钟 |
| tuple | 不可变列表有什么用? | 10 分钟 |
| dict | 键值对与哈希原理 | 15 分钟 |
| set | 去重与集合运算 | 10 分钟 |
| 可变 vs 不可变 | 引用传递陷阱在哪? | 15 分钟 |
| 深浅拷贝 | 三态差异与内存图 | 15 分钟 |
总计约 80 分钟。前四节偏"背",后两节偏"懂",后两节是面试高频,务必多花时间。
一、list:动态数组,Python 最常用的容器
1.1 创建与基本操作
list 相当于 Java 的ArrayList、C++ 的vector:有序、可变、可存放任意类型元素(Python 允许异构):
nums = [1, 2, 3] mixed = [1, "hello", 3.14, True, [5, 6]] # 甚至可以嵌套 empty = [] # 空列表增删改查五件套:
lst = [1, 2, 3] lst.append(4) # 末尾添加 → [1, 2, 3, 4] lst.insert(0, 0) # 指定位置插入 → [0, 1, 2, 3, 4] lst.remove(2) # 按值删除第一个 2 → [0, 1, 3, 4] popped = lst.pop() # 弹出并返回末尾元素 → popped=4, lst=[0, 1, 3] lst[1] = 99 # 按下标修改 → [0, 99, 3] print(len(lst)) # 31.2 切片:list 的灵魂
切片lst[start:end:step]返回新列表,遵循"左闭右开"约定(和range一致):
nums = [0, 1, 2, 3, 4, 5] print(nums[1:4]) # [1, 2, 3] 取下标 1 到 3 print(nums[:3]) # [0, 1, 2] 从头到下标 2 print(nums[3:]) # [3, 4, 5] 从下标 3 到尾 print(nums[::2]) # [0, 2, 4] 每隔一个取一个 print(nums[::-1]) # [5, 4, 3, 2, 1, 0] 反转列表面试小考点:nums[:]是什么?它是"从头到尾"的完整切片,会返回一个新的列表——也就是 list 的浅拷贝。下面讲拷贝时你还会再见到它。
1.3 常用方法速查
| 方法 | 作用 | 例子 |
|---|---|---|
lst.append(x) | 末尾添加 | lst.append(4) |
lst.extend(iter) | 批量追加 | lst.extend([5,6]) |
lst.insert(i, x) | 指定位置插入 | lst.insert(0, "a") |
lst.pop(i=-1) | 弹出元素 | lst.pop() |
lst.remove(x) | 按值删除 | lst.remove(3) |
lst.sort() | 原地排序 | lst.sort() |
lst.reverse() | 原地反转 | lst.reverse() |
lst.index(x) | 找下标 | lst.index(3) |
x in lst | 成员判断 | 3 in lst |
注意sort和reverse是原地修改,返回None——新手常写lst = lst.sort()结果得到None,这是个经典错误。
1.4 遍历的三种姿势
fruits = ["苹果", "香蕉", "橙子"] # 姿势一:直接遍历元素(最常见) for f in fruits: print(f) # 姿势二:同时要下标(enumerate) for i, f in enumerate(fruits, start=1): print(i, f) # 1 苹果 / 2 香蕉 / 3 橙子 # 姿势三:只要下标(不推荐,除非要按 index 操作其他列表) for i in range(len(fruits)): print(i, fruits[i])最佳实践:要元素就for f in lst,要"元素+序号"就for i, f in enumerate(lst)。不要写for i in range(len(...))再取lst[i],那是 C/Java 的习惯,Python 有更好的写法。
1.5 列表推导式预告
推导式是"循环 + 收集"的一行版,P03 提过,这里再补一个带条件的复杂例子:
# 取出 1-20 中能被 3 整除的数的平方 result = [x * x for x in range(1, 21) if x % 3 == 0] print(result) # [9, 36, 81, 144, 324]推导式还能嵌套:
# 生成二维坐标点 points = [(x, y) for x in range(3) for y in range(3)] print(points) # [(0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2)]判断标准:当"循环只是为了收集结果"时,就用推导式;循环体里有复杂逻辑或多条语句,才退回普通 for 循环。
1.6 list 性能注意事项
list 是动态数组,底层是一块连续内存。它的性能特点:
- 按下标访问 O(1):
lst[5]直接算地址,最快; - 末尾增删 O(1):
append/pop()平均摊还 O(1); - 中间增删 O(n):
insert(0, x)、remove(x)要移动后面所有元素; - 查找 O(n):
in、index是线性扫描。
实战建议:
# 错误:在列表头部频繁插入(每次 O(n)) lst = [] for i in range(100000): lst.insert(0, i) # 慢! # 正确:末尾添加最后反转(各 O(1),总 O(n)) lst = [] for i in range(100000): lst.append(i) lst.reverse()规模到十万级就要开始关心复杂度了。判断"元素是否存在"频繁且列表很大时,考虑转 set(O(1) 查找)换取空间换时间。
二、tuple:不可变列表
2.1 定义与特性
tuple(元组)和 list 几乎一样,唯一区别是不可变:创建后不能增删改。写法用圆括号(也可省略):
t = (1, 2, 3) t2 = 1, 2, 3 # 省略括号也一样 print(t[0]) # 1,可以读 # t[0] = 9 # TypeError: 'tuple' object does not support item assignment2.2 单元素元组的坑
(1)不是元组,是整数 1 加一个括号。要创建单元素元组,必须加逗号:
a = (1) # 这是 int b = (1,) # 这才是 tuple,长度 1 print(type(a)) # <class 'int'> print(type(b)) # <class 'tuple'>记牢口诀:单元素元组靠逗号,不靠括号。
2.3 tuple 的三大用处
用处一:不可变保证。函数返回"不会变的数据"、配置常量,用 tuple 更安全,能防止误改。
用处二:作为 dict 的键。list 可变所以不能当键(会报TypeError: unhashable type: 'list'),tuple 不可变所以可以。坐标点(x, y)当键是最典型的场景:
grid = {} grid[(0, 0)] = "起点" # tuple 当键,合法用处三:解包赋值。元组天然支持"一口气取多个值":
point = (3, 4) x, y = point # 解包 print(x, y) # 3 4这和函数返回多值时return a, b再x, y = func()是同一机制,Python 里极常用。
2.4 tuple 可以"变"吗?——半真半假
严格说 tuple 不可变,但如果 tuple 里装的是可变对象,那个对象本身可以变:
t = ([1, 2], 3) t[0].append(99) # 修改 tuple 里的 list(list 可变) print(t) # ([1, 2, 99], 3) ← 打印变了,但 t 本身没被"替换"这里变的是 list 对象的内容,不是 tuple 的结构。面试常问:“tuple 真的完全不可变吗?”——答:tuple 的引用列表不可变,但被引用的可变对象内容可以变。
2.5 namedtuple:有名字的元组
如果元组字段一多,用下标t[0]、t[1]记不住含义,可以用namedtuple(带名字的元组),它兼具元组的轻量和类的可读性:
from collections import namedtuple Point = namedtuple("Point", ["x", "y"]) p = Point(3, 4) print(p.x) # 3,用字段名访问 print(p.y) # 4 print(p[0]) # 3,也能按下标访问 x, y = p # 还能解包namedtuple 是轻量数据对象的好选择:比 dict 省内存、比 class 少样板,适合"一组固定字段的小数据"。
三、dict:键值对与哈希原理
3.1 创建与增删改查
dict 相当于 Java 的HashMap,Python 3.7+ 保证插入顺序:
d = {"name": "卡兹克", "age": 25} d["city"] = "北京" # 新增 d["age"] = 26 # 修改 print(d["name"]) # 卡兹克,读取 del d["city"] # 删除 print(d.get("city", "无")) # 无,安全读取(键不存在不报错)3.2 必会方法:get / setdefault / items
get(key, default):安全读取,键不存在返回默认值而不是抛KeyError;setdefault(key, default):键不存在则设置默认值并返回它;存在则直接返回原值——“有则用之,无则设之”;items():同时遍历键和值。
count = {} word = "apple" count[word] = count.get(word, 0) + 1 # 词频统计经典写法 d = {"a": 1, "b": 2} for key, value in d.items(): print(key, value) # a 1 / b 23.3 哈希原理一句话
dict 底层是哈希表:把键通过哈希函数变成整数,直接定位存储位置,所以查找是 O(1)。代价是键必须是可哈希的——即不可变类型(int/str/tuple/frozenset),可变类型(list/dict/set)不能当键。
3.4 更多常用方法
| 方法 | 作用 | 例子 |
|---|---|---|
d.keys() | 所有键的视图 | for k in d.keys(): |
d.values() | 所有值 | list(d.values()) |
d.items() | 键值对 | for k, v in d.items(): |
d.update(other) | 合并另一个 dict | d.update({"x": 1}) |
d.pop(key) | 删除并返回 | age = d.pop("age") |
d.clear() | 清空 | d.clear() |
key in d | 键是否存在 | "name" in d |
dict(**kwargs) | 快速创建 | dict(name="卡兹克") |
3.5 字典推导式与词频实战
字典推导式长得像列表推导式,只是外面用{}、冒号连接键值:
squares = {x: x * x for x in range(1, 6)} print(squares) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25} # 反转字典(键值互换) d = {"a": 1, "b": 2} reversed_d = {v: k for k, v in d.items()} print(reversed_d) # {1: 'a', 2: 'b'}词频统计是 dict 最经典的应用,完整流程:
text = "the quick brown fox jumps over the lazy dog the fox" words = text.split() freq = {} for w in words: freq[w] = freq.get(w, 0) + 1 # 按出现次数从高到低排序 for w, c in sorted(freq.items(), key=lambda item: item[1], reverse=True): print(w, c)freq.get(w, 0) + 1这一行是词频统计的"万能公式",背下来,笔试面试都会用到。
3.6 两个实战利器:defaultdict 与 Counter
collections模块提供了两个 dict 的强化版,写真实项目时几乎天天见:
defaultdict:给缺失键提供默认值
from collections import defaultdict # 按首字母分组单词 groups = defaultdict(list) words = ["apple", "banana", "avocado", "cherry"] for w in words: groups[w[0]].append(w) print(groups["a"]) # ['apple', 'avocado'] print(groups["z"]) # [] ← 不报错,直接给默认空列表对比普通 dict,用setdefault也能实现但更啰嗦。defaultdict 把"缺键处理"下沉到了容器本身,代码更干净。
Counter:一行搞定词频统计
from collections import Counter c = Counter("aabbccc") print(c) # Counter({'c': 3, 'a': 2, 'b': 2}) print(c.most_common(2)) # [('c', 3), ('a', 2)] 出现最多的前两个Counter 本质就是 dict(键→次数),还额外提供most_common()、+/-合并运算,是统计类任务的最终答案。面试写词频统计时,背手写版(get公式)证明基础扎实;实际工程里直接用 Counter 提高效率。
3.7 dict 的顺序性:一个历史问题
很多人有旧印象"dict 是无序的"——在 Python 3.6 之前确实如此。但从 Python 3.7 起(作为语言规范),dict 保持插入顺序:
d = {} d["first"] = 1 d["second"] = 2 d["third"] = 3 print(list(d.keys())) # ['first', 'second', 'third'] 插入顺序 # 覆盖旧值不改变顺序 d["first"] = 100 print(list(d.keys())) # ['first', 'second', 'third'] 仍是原顺序这个特性让 dict 在很多场景下可以直接当"有序结构"用。但要注意:set仍然是无序的,别把 dict 的顺序性想当然套到 set 上。
四、set:去重与集合运算
4.1 创建与去重
set 是"无顺序、不重复"的集合,同样基于哈希,成员判断 O(1)。最常用场景是去重:
nums = [1, 2, 2, 3, 3, 3] unique = set(nums) print(unique) # {1, 2, 3} print(list(unique)) # [1, 2, 3],再转回列表4.2 集合运算
set 天然支持数学里的交、并、差:
a = {1, 2, 3, 4} b = {3, 4, 5, 6} print(a & b) # {3, 4} 交集:a 和 b 共有 print(a | b) # {1,2,3,4,5,6} 并集:全部合并去重 print(a - b) # {1, 2} 差集:a 有 b 没有 print(a ^ b) # {1,2,5,6} 对称差:去掉共有成员判断:
print(3 in a) # True,O(1) 级别对比 list 的in是 O(n),数据量大时 set 优势明显。
4.3 set 的常用方法与修改操作
s = {1, 2, 3} s.add(4) # 添加 → {1, 2, 3, 4} s.add(4) # 已存在,无变化(set 自动去重) s.discard(99) # 删除不存在的元素不报错(remove 会报 KeyError) s.update([5, 6]) # 批量添加 → {1,2,3,4,5,6} s.pop() # 随机弹出一个(set 无序,别依赖这个顺序) s.clear() # 清空discard和remove的区别:remove删除不存在的元素抛KeyError,discard静默跳过。批量处理时优先用discard,避免异常打断流程。
4.4 frozenset:可哈希的 set
set 可变所以不可哈希,不能当 dict 键;frozenset是 set 的不可变版本,可哈希,可以当键:
s = frozenset([1, 2, 3]) d = {s: "元数据"} # frozenset 当键,合法 print(d[s]) # 元数据frozenset 平时用得少,面试题偶尔考到"set 能不能当 dict 键",记住答案是"不能,但 frozenset 可以"即可。
4.5 什么时候用 set、什么时候用 list?
简单决策:需要顺序/重复/按下标访问 → list;只需要"有没有"→ set;需要键值映射 → dict。set 和 dict 一样基于哈希,所以元素也必须是可哈希的。
4.6 set 实战:求共同好友
set 的集合运算在真实项目里最常见的应用之一就是"求交集"——比如找两个用户的共同好友:
my_friends = {"小明", "小红", "小刚", "小丽"} other_friends = {"小红", "小刚", "小强"} common = my_friends & other_friends print(common) # {'小红', '小刚'} 共同好友 all_friends = my_friends | other_friends print(len(all_friends)) # 5 全部好友去重后数量 # 只有我有、他没有的好友 only_mine = my_friends - other_friends print(only_mine) # {'小明', '小丽'}如果数据源是数据库或接口返回的列表,先转 set 再做运算,代码会非常干净:
liked_posts = [101, 102, 103, 104] bookmarked = [102, 104, 105] print(set(liked_posts) & set(bookmarked)) # {102, 104} 又点赞又收藏的五、可变 vs 不可变:引用传递陷阱
5.1 一张表看懂四种容器的可变性
| 容器 | 是否可变 | 是否有序 | 是否可哈希 | 查找性能 | 典型场景 |
|---|---|---|---|---|---|
| list | 可变 | 有序 | 否 | O(n) | 动态数组、保持顺序 |
| tuple | 不可变 | 有序 | 是 | O(n) | 常量数据、dict 键 |
| dict | 可变 | 插入序 | 否(键除外) | O(1) | 键值映射 |
| set | 可变 | 无序 | 否 | O(1) | 去重、集合运算 |
记忆口诀:可变不可变看"能不能原地改",可哈希不可哈希看"能不能当 dict 键",两者直接相关——可变类型一律不可哈希。
5.2b = a到底复制了什么?
这是本篇开篇问题的答案:b = a只是让b和a指向同一个对象,没有复制任何数据。
a = [1, 2, 3] b = a # b 和 a 指向同一个列表 b.append(4) print(a) # [1, 2, 3, 4] ← a 也变了!为什么 list 会这样而整数不会?
x = 5 y = x # y 和 x 都指向 5 y = 6 # 重新绑定 y,x 还是 5 print(x) # 5区别在于:list 是可变对象,b.append(4)是"原地修改";整数是不可变对象,y = 6是"重新绑定变量名"。对不可变对象,任何"修改"操作实际都是创建新对象然后重新绑定,所以另一个变量不受影响。
一句话总结:可变对象共享修改,不可变对象各自隔离。
5.3isvs==:身份与值
==比较值(内容相等);is比较身份(是不是同一个对象,内存地址相同)。
a = [1, 2, 3] b = [1, 2, 3] print(a == b) # True,内容相同 print(a is b) # False,是两个不同的对象 c = a print(a is c) # True,同一对象面试经典题:为什么256 == 256且256 is 256都为 True,但257 is 257可能是 False?因为 Python 对小整数(-5 到 256)做了缓存优化,257没有缓存,每次创建新对象。这道题考的就是is的本质——身份比较。
5.4 函数默认参数的可变陷阱(坑 1 详解)
最经典的 Python 坑之一:
def add_item(item, lst=[]): # 默认参数是可变对象! lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [1, 2] ← 竟然不是 [2]! print(add_item(3)) # [1, 2, 3]原因:默认参数在函数定义时只计算一次,所有调用共享同一个默认列表,前一次的 append 会残留。解决:用None哨兵,函数内再创建新列表:
def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst5.5 传可变对象进函数:入参也会被改
默认参数坑是"函数外引用串扰",还有一个更隐蔽的:把可变对象传给函数,函数内原地修改会直接影响外面的变量。
def add_one_to_all(nums): for i in range(len(nums)): nums[i] += 1 return nums data = [1, 2, 3] result = add_one_to_all(data) print(result) # [2, 3, 4] print(data) # [2, 3, 4] ← 原列表也被改了!这不是 bug,是 Python 的"引用传递"特性。如果你不希望外部数据被污染,有两个选择:
# 方案一:函数内先拷贝 def add_one_safe(nums): nums = nums[:] # 浅拷贝 for i in range(len(nums)): nums[i] += 1 return nums # 方案二:用列表推导式生成新列表 def add_one_clean(nums): return [n + 1 for n in nums]经验法则:函数如果"必须修改"入参就明确修改;如果只是"基于入参算新结果",一定要先拷贝或生成新对象,别让副作用悄悄扩散。
5.6 给 AI 写代码时的自查清单
现在大家都会让 AI 帮忙写代码,但 AI 最容易在可变性上翻车。给你三句自查口令,每次拿到 AI 的代码先过一遍:
- 看到
= 另一个变量,问一句:“这是在拷贝还是引用?”如果后面跟着append/sort/原地修改,八成是引用,可能污染原数据。 - 看到函数默认参数是
[]或{},直接要求 AI 改成None哨兵写法。这是 AI 高频错误。 - 看到
deepcopy,问一句:“有必要吗?”大对象深拷贝可能拖慢整个程序,优先考虑浅拷贝或直接引用。
这三条解决 90% 的可变性相关 bug。其他内容让 AI 随意发挥,但可变性这关必须人盯。
5.7 一个生活化比喻:白板与便利贴
把变量名想象成"标签",把对象想象成"东西":
- 不可变对象(int/str/tuple)像便利贴:你改不了便利贴上的内容,要"改"就撕下来换一张新的。所以
y = 6只是给y贴上新的便利贴,x手里的旧便利贴纹丝不动。 - 可变对象(list/dict/set)像白板:你可以往白板上写内容。
b = a相当于两个人看同一块白板,任何人写字,另一个人都看得见——这就是引用传递的根源。 - 浅拷贝像复印白板的清单:复印出来的是一张"写着白板上所有东西的清单",但清单指向的还是原来的白板。
- 深拷贝像搬来一块新白板并抄一遍:彻底独立,互不影响。
以后遇到"为什么它变了"的诡异问题,先在脑子里画这个比喻,再对照代码找是谁在改白板。
六、深浅拷贝:赋值/浅拷贝/深拷贝三态
6.1 三个层次的拷贝
拷贝前先明确三种操作:
| 操作 | 行为 | 效果 |
|---|---|---|
b = a | 赋值,只是引用 | 完全不复制,两个名字一个对象 |
b = a.copy()/b = a[:] | 浅拷贝 | 新建外层对象,但子对象仍共享 |
b = copy.deepcopy(a) | 深拷贝 | 新建外层对象 +递归复制所有子对象 |
6.2 浅拷贝的内存图
import copy a = [[1, 2], [3, 4]] b = a.copy() # 浅拷贝:外层新列表,内部两个小列表仍共享 b[0].append(99) # 修改共享的子列表 print(a) # [[1, 2, 99], [3, 4]] ← a 也变了! b[1] = [0, 0] # 替换 b 的第二个子列表(只动 b) print(a) # [[1, 2, 99], [3, 4]] ← a 不受影响内存指向示意:
浅拷贝内存图(b = a.copy()) a ──► [外层列表A] ──► [子列表1] ←──┐ │ │ 共享 b ──► [外层列表B] ──► [子列表1] ←──┘ └──► [子列表2] (共享)图注:浅拷贝只新建了外层列表,内层子列表 a 和 b 仍指向同一对象,所以通过 b 修改子列表会波及 a。
6.3 深拷贝完全隔离
import copy a = [[1, 2], [3, 4]] b = copy.deepcopy(a) # 深拷贝:所有层都新建 b[0].append(99) print(a) # [[1, 2], [3, 4]] ← a 完全不受影响 print(b) # [[1, 2, 99], [3, 4]]深拷贝内存指向示意:
深拷贝内存图(b = copy.deepcopy(a)) a ──► [外层A] ──► [子列表1] b ──► [外层B] ──► [新子列表1] │ │ └──► [子列表2] └──► [新子列表2] (全部独立,互不共享)图注:深拷贝递归复制每一层,a 和 b 完全隔离,改任何一方都不影响另一方。
6.4 三态对比总结
| 场景 | 用哪个 | 为什么 |
|---|---|---|
| 只想要第二个名字 | 赋值b = a | 零开销 |
| 外层独立、内层共享即可 | 浅拷贝a.copy() | 快,适合一层结构 |
| 完全独立、不许互扰 | 深拷贝deepcopy | 慢但彻底 |
| 只想复制一小段 | 切片a[:] | 即浅拷贝 |
深拷贝性能开销大:递归复制所有子对象,嵌套很深或元素很多时非常慢。所以坑 5 提醒:大数据慎用深拷贝,先想清楚"真的需要完全隔离吗",很多时候浅拷贝就够。
6.5 拷贝在 dict 和自定义对象上的应用
拷贝不仅适用于 list,dict 也一样:
d1 = {"a": [1, 2], "b": 3} d2 = d1.copy() # 浅拷贝 d2["a"].append(99) # 改共享子列表 print(d1) # {'a': [1, 2, 99], 'b': 3} ← 受影响 d3 = copy.deepcopy(d1) # 深拷贝 d3["a"].append(100) print(d1) # {'a': [1, 2, 99], 'b': 3} ← 不受影响自定义类也一样,deepcopy能递归复制类实例的内部状态。但要注意:包含文件句柄、数据库连接、网络 socket 的对象不能深拷贝(会复制出无意义的连接),这类对象应自行实现__deepcopy__控制行为,或者干脆避免拷贝。
6.6 拷贝速查:三个问题
面试官常从这三个角度连环问拷贝:
问 1:a = b[:]是什么拷贝?浅拷贝。外层独立,子对象共享。
问 2:深浅拷贝的区别一句话怎么说?深拷贝递归复制所有层,浅拷贝只复制最外层。
问 3:什么时候必须深拷贝?嵌套结构需要完全隔离时,比如把配置模板复制成多份互不影响的实例数据。
七、常见坑:5 条高频翻车记录
坑 1:函数默认参数用可变对象 → 状态串扰。前面 5.4 已详解。解决:用None哨兵,函数内再lst = []。
坑 2:a = b当拷贝用 → 数据互改。复制 list 请用a.copy()、a[:]或list(a),不要a = b。注意a = b[:]是浅拷贝,嵌套结构仍共享。
坑 3:把 list 当 dict 键 →TypeError: unhashable type: 'list'。list 可变不可哈希。解决:转 tuple,如d[tuple(lst)] = 1。
坑 4:tuple(1)报错 → 需要tuple([1])或(1,)。tuple(1)会报TypeError: 'int' object is not iterable,因为 tuple() 接收可迭代对象。单元素元组直接写(1,)。
坑 5:深拷贝性能开销大 → 大数据慎用。一个包含 10 万行数据的嵌套结构,deepcopy可能耗时数秒。解决:评估是否真的需要全量隔离,能浅拷贝就别深拷贝,能用引用就别复制。
坑 6:list(d.keys())忘记转 list → 拿到视图而非列表。d.keys()返回的是动态视图,随 dict 变化而变化,且不能直接按下标访问。要"定格"成列表必须list(d.keys())。面试若问"keys() 返回什么",答"视图 view"是加分项。
坑 7:a = b.sort()拿不到排序结果 →sort返回 None。sort/reverse原地修改并返回None,sorted(b)才返回新列表。记口诀:sort改自己,sorted出新货。
7.1 常见疑问集中答疑(6 问)
问 1:什么时候用 tuple 而不是 list?答:三个场景——①数据不该变(配置、常量、函数返回的固定结果);②要当 dict 键或放 set 里(需要可哈希);③对性能有极致要求(tuple 比 list 略省内存、创建更快)。日常写代码没有强制约束,但如果一个列表你从没增删改过它,用 tuple 更符合语义、也更安全。
问 2:dict 和 set 都是"大括号",{}到底是空 dict 还是空 set?答:是空 dict。空 set 必须写set()。因为 dict 使用太普遍,Python 把{}默认给了 dict;这个面试常考,忘一次正常,记住就再也不错了。
问 3:d.get(k)和d[k]有什么区别?答:d[k]键不存在抛KeyError;d.get(k)返回None(可传默认值d.get(k, 0))。想要"出错时快速暴露问题"用d[k],想要"安全兜底"用get。统计场景几乎必用get。
问 4:浅拷贝之后,改 a 的元素 b 会变吗?反过来呢?答:要分两层看。外层:改a[0] = xxx不会影响 b(外层独立);内层:改a[0][0]会影响 b(子对象共享)。所以"会不会互相影响"永远要问"改的是哪一层"。
问 5:为什么可变对象不能当 dict 键?答:dict 靠哈希定位。如果键可变,它哈希后存进去了,之后你改了它,哈希值变了,dict 就再也找不到它了(会变成"幽灵键")。Python 为了避免这种状态混乱,直接禁止可变类型当键。
问 6:set 和 dict 查找都是 O(1),为什么有的程序还是慢?答:哈希查找本身快,但哈希函数计算、内存寻址、冲突处理都有开销。数据量很小时(几十个元素),list 的线性查找反而可能更快(常数更小)。所以"选数据结构要看数据规模和读写比例",不要迷信 O(1)。
问 7:写完发现数据被莫名改了,怎么快速定位?答:按三条线索查:①看有没有=赋值后发生原地修改(引用);②看函数是否传入了可变对象并原地操作;③看是否用了浅拷贝共享子对象。经验上 90% 的"莫名变化"是这三个原因之一,用 5.7 的白板比喻逐条排查即可。
八、练习题/动手任务
任务 1:统计一段英文文本的词频(dict)。给一个字符串"apple banana apple cherry banana apple",用split()切成单词列表,再用count[word] = count.get(word, 0) + 1统计。验收:输出{'apple': 3, 'banana': 2, 'cherry': 1}。
任务 2:实现浅拷贝 vs 深拷贝的嵌套列表对比实验。定义a = [[1,2],[3,4]],分别用b = a.copy()和c = copy.deepcopy(a),然后对b[0]和c[0]做append(99),打印a。验收:a只受b影响不受c影响,并能口头解释原因。
任务 3:用 set 求两个列表的交集与去重。给l1 = [1,2,2,3,4]、l2 = [3,4,5],输出:①l1去重后的列表;②两列表的交集。验收:分别为[1,2,3,4]和{3,4}(交集可用 set 或 list)。
任务 4(进阶):验证is与==的差异。创建两个内容相同的列表、两个大整数(如 1000),分别用==和is比较,并打印id()看内存地址。验收:能解释为什么 list 的is为 False 而小整数缓存区间的is为 True。
任务 5:用 defaultdict 实现分组统计。给一个商品价格字典{"苹果": 3.5, "香蕉": 2.0, "橙子": 4.2, "梨": 3.8},按"价格是否超过 3 元"分组为两个列表。验收:输出形如{'便宜': ['香蕉'], '贵': ['苹果', '橙子', '梨']}的分组结果。
任务 6(综合挑战):学生成绩单管理。用 dict 存 5 个学生的成绩:{"张三": [85, 92, 78], "李四": [88, 76, 95], "王五": [90, 90, 90]},完成:①打印每个学生的平均分;②找出平均分最高的学生;③用 set 计算所有学生成绩的去重集合。验收:能输出平均分排序和去重后的成绩集合。
九、总结与下一篇预告
9.1 本篇核心结论
- list 有序可变、tuple 不可变、dict/set 基于哈希:可变性决定能不能当键,哈希决定查找快不快。
- 可变对象传递靠引用:
b = a不复制,原地修改会波及;不可变对象"修改"即重新绑定,天然隔离。 - 拷贝分深浅:赋值零拷贝、浅拷贝共享子对象、深拷贝完全隔离——按需选择,深拷贝慎用。
9.2 一张速查卡
| 问题 | 一句话答案 |
|---|---|
b = a复制了吗? | 没有,只是引用 |
| 要复制 list 怎么办? | a.copy()或a[:](浅) |
| 要完全隔离嵌套结构? | copy.deepcopy(a) |
| list 能当 dict 键吗? | 不能,转 tuple |
(1)是元组吗? | 不是,(1,)才是 |
| 去重最快的办法? | set(lst) |
==和is的区别? | 值 vs 身份 |
9.3 选容器三步决策法
写代码时不知道用哪个容器?按下面三步走:
第 1 步:要不要键值对应? → 要:dict(或 defaultdict/Counter) 第 2 步:要不要保持顺序/允许重复? → 要:list 第 3 步:只需要"存在不存在/去重"? → 要:set 剩下:数据不该变 → tuple口诀:映射找 dict,顺序找 list,存在性找 set,不变找 tuple。先定这三步,再谈拷贝、性能这些进阶话题。
9.4 本篇与系列的整体位置
这是系列第 04 篇,后面你会看到:字符串与正则(P05)大量用到"不可变对象"思想,文件读写(P06 前后)会把 dict/list 用在实际数据上。容器是 Python 的"基建",本篇的可变性与拷贝更是整个 Python 内存模型的基石,务必练熟任务 1-6 再进入下一篇。
9.5 本篇必背五句话
- list 有序可变,tuple 不可变,dict/set 哈希查找 O(1)。
b = a是引用不是拷贝;复制用copy()/[:],彻底隔离用deepcopy。- 可变类型不可哈希,不能当 dict 键;要当键就转 tuple/frozenset。
- 默认参数别写
[]/{},用None哨兵。 - 函数内改可变入参 = 改外部数据,不想污染就先生成新对象。
背完这五句,本篇的 90% 考点已经在你兜里。剩下 10% 靠练——任务 1-6 全部跑通,再去刷两道浅拷贝/深拷贝的代码阅读题,本篇就算真正吃透了。记住:数据结构的价值不在背表格,而在写代码时"选对容器、想清拷贝、防住可变性",这三件事做到位,你就已经超过很多工作一两年的人了。
【系列文章预告】下一篇我将带你玩转字符串与正则表达式:从 f-string 到 re 模块实战,敬请关注。
如果你觉得本篇有用,点个收藏方便复习;遇到"为什么变了"的诡异问题,先想想是不是可变性在作怪。我们下一篇见。
十、标签
Python数据类型Python listPython tuple深浅拷贝Python set dictPython入门Python教程
本文为《Python 基础速通——AI 时代的第一门课》系列第 04 篇。