深入理解 Python 核心机制:集合类型与可变对象详解

· 约 3 分钟 更新于 2026-09-20 ·
本文目录(13 节)

前言:为什么这些概念重要?

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, tuplelist, 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] 后,调用方为什么看不到同样的变化?

📜 原文链接:


💬 评论

这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。

正在连接评论服务,首次访问可能需要几秒…