🐍Python 语言基础
可变对象与不可变对象
面试回答
常见问法
Python 里什么是可变对象和不可变对象?为什么这件事很重要?
回答
可变对象在原地修改后,对象身份不变;不可变对象一旦”修改”,通常会创建新对象。这个区别会直接影响函数传参、副作用、共享状态和字典键可用性等问题。
# 可变对象示例
def modify_list(lst):
lst.append(4) # 原地修改
return lst
my_list = [1, 2, 3]
print(f"修改前id: {id(my_list)}")
modified = modify_list(my_list)
print(f"修改后id: {id(modified)}")
print(f"是同一个对象: {my_list is modified}") # True
# 不可变对象示例
def modify_string(s):
s = s + " world" # 创建新对象
return s
my_str = "hello"
print(f"修改前id: {id(my_str)}")
modified_str = modify_string(my_str)
print(f"修改后id: {id(modified_str)}")
print(f"是同一个对象: {my_str is modified_str}") # False
追问
- 为什么列表能原地改,字符串不能?(底层实现差异)
- 为什么不可变对象更适合当字典键?(哈希稳定性)
- 这和 Python 的对象模型有什么关系?(引用语义)
原理展开
Python 一切皆对象,变量保存的是对象引用。可变对象如 list、dict、set,允许在原对象上修改内容;不可变对象如 int、str、tuple,看似”修改”时其实更常见的是创建新对象再绑定变量。
这件事面试里很常和默认参数、浅拷贝、哈希性一起追问,所以最好把它作为对象模型的一部分来理解,而不是孤立记忆。
# 可变对象陷阱:默认参数
def append_to(num, target=[]):
target.append(num)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] # 陷阱!默认参数保留了之前的修改
# 正确做法:使用不可变默认值
def append_to_fixed(num, target=None):
if target is None:
target = []
target.append(num)
return target
print(append_to_fixed(1)) # [1]
print(append_to_fixed(2)) # [2] # 正确:每次都是新列表
# 字典键的要求
# 可变对象不能作为字典键
try:
d = {[1, 2]: "value"} # TypeError: unhashable type: 'list'
except TypeError as e:
print(f"错误: {e}")
# 不可变对象可以作为字典键
d = {(1, 2): "value", "key": 42}
print(f"字典: {d}")
# 对象身份 vs 对象值
a = [1, 2, 3]
b = [1, 2, 3]
print(f"值相等: {a == b}") # True
print(f"身份相同: {a is b}") # False
# 共享状态问题
def update_shared(data, key, value):
if isinstance(data, dict):
data[key] = value # 修改共享字典
return data
shared_dict = {}
result1 = update_shared(shared_dict, "a", 1)
result2 = update_shared(shared_dict, "b", 2)
print(f"原始字典: {shared_dict}") # {'a': 1, 'b': 2}
print(f"result1: {result1}") # {'a': 1, 'b': 2}
print(f"result2: {result2}") # {'a': 1, 'b': 2}
print(f"都是同一个对象: {shared_dict is result1 is result2}") # True
易错点
- 把变量和对象本身混为一谈
- 只背定义,不会联系参数传递和哈希
- 在需要值相等的地方用
is(如if x is 42) - 在需要身份比较的地方用
==(如if x == None)
记忆技巧
记住可变/不可变对象的区别:
- 可变对象 = “原地修改,身份不变”
- 不可变对象 = “修改即创建新对象”
常见类型分类:
- 可变:list, dict, set, 自定义类实例
- 不可变:int, float, str, tuple, frozenset, None, True, False
典型应用场景:
- 默认参数:避免可变默认值
- 字典键:必须使用不可变对象
- 函数参数:理解副作用
- 并发编程:不可变对象更安全