🐍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 一切皆对象,变量保存的是对象引用。可变对象如 listdictset,允许在原对象上修改内容;不可变对象如 intstrtuple,看似”修改”时其实更常见的是创建新对象再绑定变量。

这件事面试里很常和默认参数、浅拷贝、哈希性一起追问,所以最好把它作为对象模型的一部分来理解,而不是孤立记忆。

# 可变对象陷阱:默认参数
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

记忆技巧

记住可变/不可变对象的区别:

  1. 可变对象 = “原地修改,身份不变”
  2. 不可变对象 = “修改即创建新对象”

常见类型分类:

  • 可变:list, dict, set, 自定义类实例
  • 不可变:int, float, str, tuple, frozenset, None, True, False

典型应用场景:

  • 默认参数:避免可变默认值
  • 字典键:必须使用不可变对象
  • 函数参数:理解副作用
  • 并发编程:不可变对象更安全