前言:为什么这些概念重要?
Python 是一门一切皆对象的语言。理解 “集合类型”(如 set / frozenset)以及 “可变与不可变对象” 的本质区别,对于写出更高效、更安全的代码至关重要。
本文将通过简洁示例,带你掌握这些核心机制,在日常编程中更加得心应手。
一、Python 集合类型详解(set / frozenset)
1.1 集合基础概念
| 类型 | 可变性 | 是否可哈希 | 特点 |
|---|---|---|---|
set | ✅ | ❌ | 无序、元素唯一、可增删 |
frozenset | ❌ | ✅ | 不可修改,可作为字典键或集合元素 |
集合元素必须可哈希,不能把 hashable 直接等同于不可变。元组只有在每个元素都可哈希时才可哈希,例如 (1, "a") 可以,(1, []) 不可以。自定义对象也有自己的哈希规则。参见 Python 可哈希对象定义。
1.2 创建集合
s1 = set() # 空集合
s2 = {1, 2, 3} # 非空集合
fs = frozenset([1, 2]) # 冻结集合
⚠️ 注意:{} 表示空字典,创建空集合必须用 set()。
1.3 常用操作
✅ 添加元素
s = {1, 2}
s.add(3) # 添加单个元素
s.update([4, 5]) # 添加多个元素
❌ 删除元素
s.remove(2) # 若不存在会报错
s.discard(100) # 若不存在不会报错
s.pop() # 移除任意一个元素,不是随机抽样接口
🔍 成员检测 & 遍历
print(3 in s) # True / False
for item in s:
print(item) # 遍历元素(无序)
🧊 frozenset 特性
fs = frozenset([1, 2])
fs.add(3) # ❌ 报错:'frozenset' object has no attribute 'add'
二、Python 可变与不可变对象机制
2.1 不可变类型(immutable)
不可变对象在被“修改”时其实是创建了新对象,原始对象不变。
| 类型 | 是否可变 |
|---|---|
int, float | ❌ |
str, bool | ❌ |
tuple | ❌ |
frozenset | ❌ |
None, bytes | ❌ |
a = "hello"
print(id(a)) # 1001
a += " world"
print(id(a)) # 1002(生成新对象)
2.2 可变类型(mutable)
可变对象修改内容时不会改变内存地址,原地更新。
| 类型 | 是否可变 |
|---|---|
list | ✅ |
dict | ✅ |
set | ✅ |
| 自定义对象 | ✅(默认) |
lst = [1, 2]
print(id(lst)) # 2001
lst.append(3)
print(id(lst)) # 2001(地址不变)
2.3 二者区别对比
| 维度 | 不可变对象 | 可变对象 |
|---|---|---|
| 内存地址变化 | 修改会生成新对象 | 原地修改 |
| 是否可哈希 | 还要看类型与内部元素 | list/dict/set 不可哈希;自定义对象另论 |
| 示例类型 | int, str, tuple | list, dict, set |
| 函数参数行为 | 重新绑定形参不修改调用方绑定 | 原地修改会被共享该对象的调用方看到 |
def f(x):
x.append(1)
a = [0]
f(a)
print(a) # [0, 1] → 原列表变了
图解:本图针对嵌套列表例子。赋值绑定、浅拷贝和深拷贝是不同操作。
判断是否共享不能只比较两个列表的值。先检查 a is b,再检查 a[0] is b[0]。浅拷贝后的外层身份不同,但内层仍可能相同。tuple 不允许替换自身元素,却可以引用一个可变列表;“tuple 不可变”并不等于整个引用图不可变。
动手核对: 分别执行追加内层元素和替换 b[0],预测 a 的变化,再用断言验证。把 tuple 中放入列表后尝试 hash,解释失败与内部可变引用的关系。
三、实用建议与踩坑提醒
- ✅ 优先使用不可变对象作为字典键或集合元素;
- ⚠️ 修改可变对象时注意函数副作用,可使用
.copy()避免污染原始数据; - ❌ 不要将可变对象作为默认参数,例如:
def f(lst=[]); - 🧊 使用
frozenset构建只读集合或做缓存键值对;
四、用嵌套列表检查拷贝边界
列表的 .copy() 只复制外层,内层对象仍共享。先判断需要隔离哪一层,再决定是否深拷贝:
original = [[1], [2]]
copied = original.copy()
copied[0].append(3)
assert original[0] == [1, 3]
assert copied is not original
可复跑断言与输出
仓库 examples/blog/run-examples.py 中的 copy_check() 同时核对容器身份、内层列表身份和最终内容,而不是仅比较 print 输出。本次结果:原对象和浅拷贝均为 [[1,3],[2]],深拷贝仍为 [[1],[2]],见运行记录。
检查身份回答“共享的是哪一层”,检查内容回答“这次修改影响了谁”。若只看最终值,两份独立但内容相同的列表也会相等,无法证明没有共享引用。深拷贝适合此例,但遇到文件句柄、网络连接或自定义 __deepcopy__ 时,应重新定义复制语义。
结语:理解这些,Python 才算“入门”完成
集合、可变性、引用、哈希性是 Python 中最容易忽略但最根本的概念。熟练掌握它们,能让你写出更安全、性能更高、更易维护的 Python 程序。
复习时试着解释:为什么 hash((1, [])) 会失败?函数里把 x.append(1) 改成 x = x + [1] 后,调用方为什么看不到同样的变化?
📜 原文链接:
💬 评论
这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。