☰
Python 基础速通04-Python 四大容器 list/tuple/dict/set 对比:可变性、性能、拷贝问题面试必问
2026/10/15 5:32:47 网站建设 项目流程

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)) # 3

1.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 assignment

2.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 2

3.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)合并另一个 dictd.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 lst

5.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 的代码先过一遍:

  1. 看到= 另一个变量,问一句:“这是在拷贝还是引用?”如果后面跟着append/sort/原地修改,八成是引用,可能污染原数据。
  2. 看到函数默认参数是[]或{},直接要求 AI 改成None哨兵写法。这是 AI 高频错误。
  3. 看到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 本篇核心结论

  1. list 有序可变、tuple 不可变、dict/set 基于哈希:可变性决定能不能当键,哈希决定查找快不快。
  2. 可变对象传递靠引用:b = a不复制,原地修改会波及;不可变对象"修改"即重新绑定,天然隔离。
  3. 拷贝分深浅:赋值零拷贝、浅拷贝共享子对象、深拷贝完全隔离——按需选择,深拷贝慎用。

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 本篇必背五句话

  1. list 有序可变,tuple 不可变,dict/set 哈希查找 O(1)。
  2. b = a是引用不是拷贝;复制用copy()/[:],彻底隔离用deepcopy。
  3. 可变类型不可哈希,不能当 dict 键;要当键就转 tuple/frozenset。
  4. 默认参数别写[]/{},用None哨兵。
  5. 函数内改可变入参 = 改外部数据,不想污染就先生成新对象。

背完这五句,本篇的 90% 考点已经在你兜里。剩下 10% 靠练——任务 1-6 全部跑通,再去刷两道浅拷贝/深拷贝的代码阅读题,本篇就算真正吃透了。记住:数据结构的价值不在背表格,而在写代码时"选对容器、想清拷贝、防住可变性",这三件事做到位,你就已经超过很多工作一两年的人了。

【系列文章预告】下一篇我将带你玩转字符串与正则表达式:从 f-string 到 re 模块实战,敬请关注。

如果你觉得本篇有用,点个收藏方便复习;遇到"为什么变了"的诡异问题,先想想是不是可变性在作怪。我们下一篇见。


十、标签

Python数据类型Python listPython tuple深浅拷贝Python set dictPython入门Python教程


本文为《Python 基础速通——AI 时代的第一门课》系列第 04 篇。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询