Python 可变(Mutable)与不可变(Immutable)数据类型 是 Python 中非常核心的概念,它直接影响代码的行为、内存使用、函数参数传递、哈希表键的选择等。
下面从定义、分类、特点、常见问题到实际使用场景,一次性讲清楚。
1. 核心概念
- 不可变类型(Immutable)
创建后内容无法修改(不能就地改变)。
任何“修改”操作都会产生一个全新的对象。 - 可变类型(Mutable)
创建后内容可以就地修改(in-place modification),对象本身 id 不变。
2. Python 常见数据类型的可变/不可变分类
| 数据类型 | 是否可变 | 例子 | id 是否改变(修改后) | 是否可作为 dict 的 key |
|---|---|---|---|---|
| int | 不可变 | 5, -3, 0 | 是 | 是 |
| float | 不可变 | 3.14, -0.0 | 是 | 是 |
| bool | 不可变 | True, False | 是 | 是 |
| str | 不可变 | “hello”, ‘python’ | 是 | 是 |
| tuple | 不可变 | (1, 2, 3), (“a”,) | 是 | 是(元素必须都不可变) |
| frozenset | 不可变 | frozenset([1,2,3]) | 是 | 是 |
| list | 可变 | [1, 2, 3], [“a”, “b”] | 否 | 否 |
| dict | 可变 | {“a”:1, “b”:2} | 否 | 否 |
| set | 可变 | {1, 2, 3} | 否 | 否 |
| bytearray | 可变 | bytearray(b”abc”) | 否 | 否 |
3. 关键区别演示(代码对比)
# 不可变类型:修改会产生新对象
a = 100
print(id(a)) # 例如:140714123456789
a = a + 1 # 实际是创建新对象
print(id(a)) # id 变了,例如:140714123457021
s = "hello"
print(id(s)) # id1
s = s + " world" # 创建新字符串
print(id(s)) # id 不同
t = (1, 2, 3)
print(id(t))
# t[0] = 100 # TypeError: 'tuple' object does not support item assignment
t = (100, 2, 3) # 重新赋值,id 改变
print(id(t))
# 可变类型:可以就地修改,id 不变
lst = [1, 2, 3]
print(id(lst)) # id1
lst.append(4) # 就地修改
lst[0] = 100
print(id(lst)) # id 仍然是 id1
d = {"a": 1}
print(id(d))
d["b"] = 2 # 就地修改
print(id(d)) # id 不变
4. 最容易踩的坑(函数参数传递)
Python 是对象引用传递,但可变与不可变表现不同:
def modify(x):
x += 1 # 对于不可变类型,x 指向新对象
print("inside:", x, id(x))
num = 5
print("before:", num, id(num))
modify(num)
print("after: ", num, id(num)) # num 不变!
# 输出:
# before: 5 140714123456789
# inside: 6 140714123457021
# after: 5 140714123456789
def modify_list(lst):
lst.append(999) # 就地修改,影响外部对象
mylist = [1, 2, 3]
print("before:", mylist)
modify_list(mylist)
print("after: ", mylist) # 被修改了!
# 输出:
# before: [1, 2, 3]
# after: [1, 2, 3, 999]
防御性编程建议:
当你不希望函数修改外部可变对象时,建议在函数内部创建副本:
def safe_modify(lst):
lst = lst.copy() # 或 lst[:]
lst.append(999)
return lst
5. 为什么 tuple 是不可变的,但可以包含可变对象?
t = (1, [2, 3], 4)
# t[0] = 100 # 报错
t[1].append(99) # 合法!修改的是 list 的内容
print(t) # (1, [2, 3, 99], 4)
结论:
tuple 本身不可变,指的是它引用的对象集合不能改变(不能增删改元素),但如果元素本身是可变对象,则该元素的内容仍然可以修改。
因此:
想作为 dict 的 key 或放入 set 时,tuple 元素必须全部是不可变类型。
# 合法
d = {(1, 2): "point"}
# 非法
# d = {(1, [2]): "point"} # TypeError: unhashable type: 'list'
6. 总结对比表(面试/刷题常考)
| 问题场景 | 推荐做法 / 结论 |
|---|---|
| 需要修改内容 | 用 list、dict、set |
| 需要作为 dict 的 key 或 set 元素 | 用 str、int、tuple(元素必须不可变)、frozenset |
| 需要保证数据不被意外修改 | 用 tuple、frozenset、str |
| 函数参数不想被修改 | 传不可变对象,或在函数内 copy 可变对象 |
| 追求哈希性能(dict/set key) | 优先用不可变类型 |
| 内存效率(小整数、短字符串) | Python 有小整数池和字符串驻留,相同值 id 可能相同 |
7. 一句话记忆口诀
- 不可变:int、float、str、tuple、bool、frozenset
→ 值不变 → id 可能变 → 安全做 key - 可变:list、dict、set、bytearray
→ 内容可改 → id 不变 → 慎做 key
有任何具体场景(比如函数参数陷阱、tuple 嵌套可变对象、自定义类的可变性、copy 与 deepcopy 的区别等)想深入讨论,都可以继续问我!