人人都会AI编程

7.3 可变与不可变类型的本质区别与选型

更新时间:2026-07-12

理解了 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)),可变元素的内容还是可以被修改。

实际影响:为什么我们必须关心

① 赋值与“修改”行为不同

  • 对不可变对象执行类似修改的操作(如字符串切片替换、整型自增),都会返回新对象,必须重新赋值才能保留变化。
  • 对可变对象执行 appendremove 等方法,原对象被原地修改,共享该对象的所有引用都会看到变化。

② 作为函数参数时的副作用风险

  • 将可变对象作为函数参数传递,函数内修改该对象会影响外部。这是很多 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

理解并善用可变与不可变类型,是写出无副作用、安全、可预测代码的重要基础。它并不复杂,但需要在实际编码中刻意练习和留心。