理解了 Python 的六大标准数据类型之后,必须想清楚一个关键问题:为什么有的对象可以被修改,有的不能? 这就是可变与不可变类型的区别。掌握了它,才能避免很多隐蔽的坑,并且在合适场景选择合适的数据结构。
本质区别:对象的值是否能在原地修改
- 不可变类型:对象创建后,值不能改变。如果想“修改”,实际上是创建了一个新对象,把变量指向新对象,原对象还在内存中(如果没有其他引用,则被回收)。
- 可变类型:对象创建后,值可以在原地改变,内存地址不变,变量仍然指向同一个对象。
可以用一句话概括:可变类型允许修改对象内部状态而不改变其标识(id),不可变类型任何“修改”都会产生新对象。
验证方法很简单,使用 id() 查看内存地址:
# 不可变示例:int
a = 10
print(id(a)) # 比如 140734123456789
a = a + 1
print(id(a)) # 地址变了,是新对象
# 可变示例:list
b = [1, 2, 3]
print(id(b)) # 比如 3074323456
b.append(4)
print(id(b)) # 地址不变,还是原对象
Python 中常见类型的分类
| 分类 | 不可变类型 | 可变类型 |
|------|-----------|----------|
| 数值 | int, float, complex, bool | 无 |
| 序列 | str, tuple, bytes | list, bytearray |
| 映射 | 无 | dict |
| 集合 | frozenset | set |
| 其他 | None | 大部分自定义类实例(默认) |
要特别注意:tuple 是不可变的,但其内部如果有可变元素(如 ([1,2], 3)),可变元素的内容还是可以被修改。
实际影响:为什么我们必须关心
① 赋值与“修改”行为不同
- 对不可变对象执行类似修改的操作(如字符串切片替换、整型自增),都会返回新对象,必须重新赋值才能保留变化。
- 对可变对象执行
append、remove等方法,原对象被原地修改,共享该对象的所有引用都会看到变化。
② 作为函数参数时的副作用风险
- 将可变对象作为函数参数传递,函数内修改该对象会影响外部。这是很多 bug 的源头。
- 不可变对象作为参数传递则安全,不会影响外部变量。
def add_item(lst, item):
lst.append(item) # 会修改传入的列表
my_list = [1, 2]
add_item(my_list, 3)
print(my_list) # [1, 2, 3],被改了
③ 字典的键必须是不可变类型
- 字典用哈希值实现快速查找,要求键的哈希值在其生命周期内不能变化。因此,只有不可变类型才能作为字典的键(或者带不可变特性的自定义对象)。
- 列表、字典等可变类型直接作为键会抛出
TypeError。
④ 多线程安全性
- 不可变对象天然线程安全,无锁也无需担心数据竞争。
- 多个线程共享同一个可变对象则需要加锁保护,否则可能出现数据错乱。
选型指南:什么时候用什么
- 需要存储多个值,且可能会增删改:首选 list(有序)或 set(无序、去重)。
- 需要保证数据不可篡改:使用 tuple 替代 list,既可防止意外修改,又可作为字典键。
- 字符串拼接频繁时:避免用
+反复创建新字符串(不高效),应收集到 list 然后用''.join()合并。 - 函数参数默认值:绝不能用可变对象(如
def f(lst=[])),因为默认参数在函数定义时已创建,多次调用会共享同一对象,造成意外累积。 - 需要在多个地方共享同一个数据容器且保持同步:可以用可变对象,但要清楚意识到修改会互相影响。
- 追求简洁不可变数据:可以用
dataclass(frozen=True)创建不可变数据类,或使用frozenset。
理解并善用可变与不可变类型,是写出无副作用、安全、可预测代码的重要基础。它并不复杂,但需要在实际编码中刻意练习和留心。