知识点

Python对象可变性

Python中的变量保存对对象的引用。判断一次修改会影响谁,需要同时看对象是否允许原地改变,以及多个变量是否指向同一个对象。

定义

对象创建后允许原地改变内容的,称为可变对象;创建后内容不能原地改变,只能产生另一个对象并重新绑定的,称为不可变对象。

变量赋值b = a绑定的是对象引用。若a指向可变对象,b也指向同一对象,那么通过任一变量原地修改,两个变量都会观察到变化。若对象不可变,例如字符串,运算会产生新对象并让变量指向新对象,原对象保持不变。

性质

常见类型

类型可变性备注
int、float、bool、complex不可变数值运算产生新对象
str不可变字符串操作产生新字符串
tuple不可变元组保存的引用不能替换,引用目标的内容仍可能改变
list可变可以原地追加、删除或替换元素
dict可变键必须是可哈希对象
set可变元素必须是可哈希对象
frozenset不可变不可变集合,可以作为字典的键

“不可变”描述对象自身能否原地改变,不表示对象引用的所有子对象都不可变。例如元组t = ([1],) 中的列表仍可执行t[0].append(2),因为这里修改的是列表;而t[0] = [2] 试图替换元组保存的引用,会抛出TypeError。

别名与复制

赋值b = a创建的是别名,不会复制对象。需要独立列表时,可以使用浅复制b = a[:] 或b = list(a)。浅复制会建立新的外层列表,但列表中的元素引用仍与原列表相同。

alias.py
a = [1, 2]
b = a
b.append(3)
print(a) # [1, 2, 3]

重新绑定

不可变对象上的“修改”会重新绑定变量:

rebind.py
s = "ab"
t = s
t += "c"
print(s) # ab
print(t) # abc

t += "c" 没有改变原来那个字符串,而是创建一个新字符串并让t指向它,因此s看到的仍是ab。

例子

修改共享列表

a = [1, 2]、b = a后,a is b为真。执行b.append(3) 后,两个变量都看到 [1, 2, 3],因为它们引用同一个列表对象。

切片建立独立外层列表

a = [1, 2]、b = a[:] 后,a is b为假。执行b.append(3) 只改变b,a仍是 [1, 2]。如果列表元素本身也是可变对象,浅复制后两个外层列表仍可能共享这些子对象。

元组内的可变元素

在t = ([1],) 中,t[0].append(2) 合法,结果中元组的第一个引用仍指向同一个列表;t[0] = [2] 不合法,因为它要替换元组保存的引用。

探索

切换共享引用、复制列表和字符串重新绑定,观察变量箭头指向的对象何时相同。

赋值之后,为什么有时两个变量会一起改变?

比较共享的可变对象、独立副本和不可变对象的重新绑定。

正在准备演示…

题集

练习

  1. 关于可变性,下列说法正确的是:

    题 mutability-claims · choice
    参考解答

    元组不可变只约束它保存的引用,元组里的列表内容仍可修改,B错;切片返回新列表,不是别名,D错。

  2. 设a = [1, 2],执行b = a与b.append(3) 后,a的值是:

    选择答案
    参考解答

    b = a只复制引用,两者指向同一个列表;通过b原地追加,a也会看到新元素。

  3. 设s = "ab",执行t = s和t += "c" 后,s的值是:

    选择答案
    参考解答

    字符串不可变,+= 让t绑定到新字符串abc,原来的ab没有被修改。