21 KiB
sources, brief
| sources | brief | |||||||
|---|---|---|---|---|---|---|---|---|
|
Python 不可变对象创建后不能原地修改,变量只能重新绑定到新对象。 |
Python 不可变对象
本页边界
本页专注字符串、元组、数字等对象为什么不能原地修改,以及重新绑定和新对象创建的区别。变量绑定的通用规则见 concepts/变量绑定;可变对象的相反行为见 concepts/Python-可变对象;复制和共享引用的整体视角见 concepts/Python-拷贝语义。
Python 不可变对象(immutable object)是指对象一旦创建,其内部值就不能被原地修改。对不可变对象执行看似“修改”的操作时,Python 通常会创建一个新对象,并让变量名重新绑定到这个新对象。
这一概念在 summaries/04_Strings 中通过字符串 str 得到了重点说明,也在 summaries/01_Datatypes 中通过元组 tuple 得到了进一步体现。summaries/07_Objects 则从 Python 对象模型角度补充了更底层的解释:变量是名字,不是内存位置;赋值不会复制对象,只会复制引用。 因此,不可变性是理解 Python对象模型、可变性与引用 和 拷贝语义 的关键基础。
核心含义
不可变对象的关键特征是:
- 对象创建后,其值不能被直接改变。
- 不能通过索引或属性原地修改对象内部内容。
- 所有修改型操作都会生成新对象,或要求显式构造新对象。
- 变量名可以重新绑定到新对象,但这不等于原对象被修改。
- 多个变量可以引用同一个不可变对象,但由于对象不能被原地修改,共享通常更安全。
例如字符串是不可变对象:
s = 'Hello World'
s[1] = 'a'
这会报错:
TypeError: 'str' object does not support item assignment
因为字符串不支持对单个字符进行原地赋值。
元组也是不可变对象:
s = ('GOOG', 100, 490.1)
s[1] = 75
这同样会报错:
TypeError: 'tuple' object does not support item assignment
这说明不可变性不只出现在文本数据中,也出现在用于组织记录的结构化数据中。
变量是名字,不是内存位置
summaries/07_Objects 强调了一个重要原则:变量是名字,不是内存位置。
例如:
s = 'Hello'
s = 'World'
这并不是把 'Hello' 这个字符串对象改成 'World',而是让变量名 s 从旧字符串对象重新绑定到另一个字符串对象。
类似地:
t = ('AA', 100, 32.2)
t = ('AA', 75, 32.2)
这不是把旧元组的第二个元素改成 75,而是创建了一个新的元组,并让 t 指向它。
因此,理解不可变对象时必须区分:
- 对象本身:内存中真实存在的值,有自己的类型和身份。
- 变量名:指向对象的名字或引用,可以重新绑定。
- 赋值:让名字引用某个对象,而不是把对象内容复制或覆盖。
这与 concepts/变量绑定 和 concepts/Python-对象模型 密切相关。
赋值不会复制对象
Python 中许多操作本质上都是“赋值”或“存储引用”:
a = value
s[n] = value
s.append(value)
d['key'] = value
summaries/07_Objects 指出:这些操作都不会复制被赋的值,而只是复制对象引用。
对于可变对象,这种引用共享可能导致意外修改:
a = [1, 2, 3]
b = a
a.append(999)
b # [1, 2, 3, 999]
因为 a 和 b 指向同一个列表对象,修改列表会通过所有引用可见。
但对于不可变对象,共享同一个对象通常不会产生同类风险:
a = 'Hello'
b = a
a = a + ' World'
b # 'Hello'
a # 'Hello World'
这里 a + ' World' 创建了新字符串,a 被重新绑定到新对象;b 仍然引用原来的 'Hello'。原字符串没有被修改,也不可能被原地修改。
这也是 summaries/07_Objects 提到基础类型如 int、float、str 设计为不可变的重要原因之一:共享不可变对象更安全,不容易因为某处代码原地修改而破坏其他地方的数据。
对象身份、相等性与不可变对象
Python 中可以用 is 判断两个变量是否引用同一个对象:
a = [1, 2, 3]
b = a
a is b # True
对象身份也可以用 id() 查看:
id(a)
id(b)
如果两个变量引用同一个对象,它们的 id() 相同。
不过,通常应该使用 == 比较对象值,而不是用 is 比较身份:
a = [1, 2, 3]
b = [1, 2, 3]
a is b # False
a == b # True
对于不可变对象,也要区分“值相等”和“同一个对象”:
x = 'Hello'
y = 'Hello'
x == y # True
至于 x is y 是否为 True,可能受解释器优化、对象驻留等因素影响,不应作为普通值比较的依据。一般原则是:
- 用
==比较值是否相等。 - 用
is判断是否为同一个对象,典型场景是x is None。
这部分内容连接到 Python对象模型 与 可变性与引用。
字符串中的不可变性
在 summaries/04_Strings 中,字符串被明确描述为“immutable”或只读对象。创建字符串后,不能直接修改其中某个字符。
例如:
symbols = 'AAPL,IBM,MSFT,YHOO,SCO'
symbols[0] = 'a'
这不会把字符串改成小写开头,而是直接抛出 TypeError。
如果想得到修改后的文本,需要创建一个新字符串:
symbols = 'AAPL,IBM,MSFT,YHOO,SCO'
symbols = symbols + ',GOOG'
这里并不是原字符串被追加了 ',GOOG',而是:
- 表达式
symbols + ',GOOG'创建了一个新字符串。 - 变量名
symbols重新绑定到这个新字符串。 - 原来的字符串如果没有其他引用,之后会被垃圾回收。
字符串方法也不会原地修改
字符串的各种方法都会返回新字符串,而不是修改原字符串。
例如:
s = ' Hello '
t = s.strip()
结果:
s # ' Hello '
t # 'Hello'
strip() 返回去除首尾空白后的新字符串,但原始字符串 s 保持不变。
类似地:
s = 'Hello'
s.lower() # 'hello'
s # 'Hello'
如果要保存结果,必须重新赋值:
s = s.lower()
相关内容可见 Python字符串 和 Python字符串方法。
元组中的不可变性
在 summaries/01_Datatypes 中,元组被用来表示股票持仓这样的简单记录:
s = ('GOOG', 100, 490.1)
这个元组包含三个部分:
- 股票代码:
'GOOG' - 股数:
100 - 价格:
490.1
元组内容是有序的,可以通过索引访问:
name = s[0]
shares = s[1]
price = s[2]
但元组内容不能原地修改:
s[1] = 75
# TypeError: object does not support item assignment
如果要把股数从 100 改为 75,需要创建一个新的元组:
s = (s[0], 75, s[2])
这里看起来像是“修改了 s”,但实际发生的是:
- 读取旧元组中的股票代码和价格。
- 用新的股数
75构造一个全新的元组。 - 变量名
s从旧元组重新绑定到新元组。 - 旧元组本身没有被修改。
这与字符串拼接、字符串方法返回新字符串的机制一致,核心都是变量重新绑定,而不是对象原地变化。
元组作为不可变记录
元组常用于表示一个由多个字段组成的单一对象,类似数据库表中的一行:
record = ('GOOG', 100, 490.1)
它适合表示结构固定、字段数量明确、无需频繁修改的简单记录。这与 元组 和 Python数据结构 密切相关。
元组还支持打包与解包:
t = ('AA', 75, 32.2)
name, shares, price = t
解包后,变量 name、shares、price 分别引用元组中的值。若想基于这些变量重新组织数据,也是在创建新元组:
t = (name, 2 * shares, price)
这个例子再次体现:不可变对象本身不变,但可以用旧值计算或组合出新对象。
不可变对象与拷贝语义
summaries/07_Objects 介绍了浅拷贝和深拷贝:
- 浅拷贝只复制外层容器,内部对象仍然共享。
- 深拷贝会递归复制嵌套对象。
例如列表浅拷贝:
a = [2, 3, [100, 101], 4]
b = list(a)
a[2].append(102)
b[2] # [100, 101, 102]
这里 a 和 b 是不同的外层列表,但共享内部列表 [100, 101],所以修改内部列表会影响两边。
不可变对象在这种场景中通常更安全。若共享的是字符串、整数、浮点数或不可变元组,无法被原地修改,因此共享引用本身不会造成“某处修改、处处变化”的问题。
不过需要注意:元组不可变指的是元组本身不能增删改元素引用,不代表其内部引用的对象一定不可变。
例如:
t = ('AA', [100, 101])
t[1].append(102)
t # ('AA', [100, 101, 102])
这里没有修改元组的元素引用,但修改了元组内部列表对象的内容。因此,若要获得真正稳定的不可变数据结构,内部元素也应尽量是不可变对象。
这与 拷贝语义、可变性与引用 和 Python数据结构 相关。
常见不可变对象
Python 中常见的不可变对象包括:
str:字符串int:整数float:浮点数bool:布尔值tuple:元组bytes:字节串frozenset:不可变集合NoneType:None
在 summaries/04_Strings 中,重点涉及的是:
str:文本字符串,不可原地修改。bytes:字节串,也具有类似序列特征,但表示的是字节数据。
在 summaries/01_Datatypes 中,重点涉及的是:
tuple:有序、固定、不可原地修改的数据组合。None:表示缺失值或占位值的特殊对象。int和float:用于数值计算的基本不可变数值类型。
在 summaries/07_Objects 中,这些类型进一步被放入 Python 的统一对象模型中理解:数字、字符串、元组、列表、函数、模块、异常、类和实例等都是对象,只是不同对象具有不同的可变性和行为。
与可变对象的对比
不可变对象和可变对象的区别在于能否原地修改内部内容。
字符串不可变:
s = 'abc'
s[0] = 'A' # TypeError
元组不可变:
t = ('AA', 100, 32.2)
t[1] = 75 # TypeError
列表可变:
items = ['a', 'b', 'c']
items[0] = 'A'
items # ['A', 'b', 'c']
字典也可变:
d = {
'name': 'AA',
'shares': 100,
'price': 32.2,
}
d['shares'] = 75
修改后,字典对象本身被原地更新:
{'name': 'AA', 'shares': 75, 'price': 32.2}
因此,在 summaries/01_Datatypes 的股票持仓例子中:
- 如果使用元组表示持仓记录,修改股数需要创建新元组。
- 如果使用字典表示持仓记录,可以直接通过键修改字段。
这也体现了 元组 与 字典 在数据建模上的差异:元组更适合固定结构的简单记录,字典更适合字段较多、需要具名访问、可能频繁修改的记录。
这与 Python序列、列表、Python数据类型 和 Python数据结构 等主题相关。
拼接、替换和重建为何会生成新对象
对于字符串:
s = 'Hello'
s = s + ' World'
或:
s = 'Hello world'
s = s.replace('Hello', 'Hallo')
这些操作都不会修改原始字符串,而是创建新字符串。
对于元组:
t = ('AA', 100, 32.2)
t = (t[0], 75, t[2])
这也不会修改原始元组,而是创建新元组。
这也是为什么字符串大量重复拼接时可能带来性能问题:每次拼接都可能产生新对象。更高效的方式通常是把多个字符串放入列表,然后使用 join() 合并:
parts = ['AAPL', 'IBM', 'MSFT']
text = ','.join(parts)
这与 Python文本处理 和 Python字符串方法 相关。
类型属于对象,不属于变量名
summaries/07_Objects 还强调:变量名本身没有类型,类型属于对象值。
a = 42
b = 'Hello World'
type(a) # int
type(b) # str
之后变量名可以重新绑定到其他类型的对象:
a = 'now a string'
这并不是 int 对象变成了字符串,而是变量名 a 改为引用一个新的字符串对象。
理解这一点有助于避免把“变量的变化”误解为“对象的变化”。不可变对象不允许内部值原地改变,但变量名始终可以重新绑定到任意对象。
类型检查可以用 isinstance():
if isinstance(a, str):
print('a is a string')
但不应过度依赖类型检查,否则容易增加代码复杂度。
不可变性的好处
不可变对象带来一些重要优势。
1. 更安全
对象不会被意外修改,因此在多个地方共享同一个对象时更安全。
例如多个变量引用同一个字符串,不必担心其中一个变量会改变该字符串本身。多个变量引用同一个只包含不可变元素的元组时,也不必担心元组中的字段被某处代码原地改写。
这正好回应了 summaries/07_Objects 中关于共享可变对象的警告:如果不了解引用共享,可能会以为自己在修改“私有副本”,实际上却破坏了程序其他部分也在使用的数据。不可变对象能显著降低这类风险。
2. 可哈希
许多不可变对象可以作为字典键或集合元素,例如字符串、整数、元组等:
prices = {
'IBM': 91.1,
'AAPL': 150.0,
}
字符串之所以适合作为字典键,一个重要原因就是其值不可变。
需要注意的是,元组是否可哈希还取决于其内部元素是否也可哈希。例如只包含字符串、整数、浮点数的元组通常可以作为键;包含列表的元组则不能作为键。
3. 行为更可预测
不可变对象让程序状态更稳定,减少隐藏副作用。调用字符串方法时,不会悄悄改变原字符串,而是显式返回新字符串。基于元组构造新记录时,也不会意外改变旧记录。
4. 适合表达固定结构
元组的不可变性使它适合表示结构固定的记录,例如:
('GOOG', 100, 490.1)
这类数据更像“一条完整记录”,而不是一组需要随时增删改的元素。
常见误区
误区一:重新赋值就是修改对象
s = 'Hello'
s = 'World'
这不是把 'Hello' 改成 'World',而是让变量 s 指向另一个字符串对象。
元组同理:
t = ('AA', 100, 32.2)
t = ('AA', 75, 32.2)
这不是修改第一个元组,而是让 t 绑定到另一个元组。
误区二:字符串方法会修改原字符串
s = 'Hello'
s.lower()
print(s) # 仍然是 'Hello'
如果没有接收返回值,转换结果会被丢弃。
正确写法:
s = s.lower()
误区三:拼接违反了不可变性
symbols = symbols + ',GOOG'
这不是原地追加,而是创建新字符串并重新绑定变量。
误区四:元组像列表,所以也能修改元素
元组和列表都属于序列,都可以用索引访问元素:
t = ('AA', 100, 32.2)
t[1]
但元组不是列表。列表支持元素赋值,元组不支持:
t[1] = 75 # TypeError
如果数据需要频繁修改,应考虑使用列表、字典或其他可变结构;如果数据是一条结构固定的记录,元组通常更合适。
误区五:不可变容器内部一定完全不可变
元组本身不可变,但元组可以包含可变对象:
t = ([1, 2], 'x')
t[0].append(3)
这不会替换 t[0],但会修改 t[0] 所引用的列表对象。因此,判断数据是否“真正不可变”时,要同时考虑容器和内部元素。
误区六:is 可以用来比较不可变对象的值
即使两个不可变对象的值相等,也不应该依赖 is 判断值相等:
a = 'hello'
b = 'hello'
a == b # 推荐:比较值
is 比较对象身份,不是一般意义上的值相等。
与 04_Strings 的关系
summaries/04_Strings 中围绕字符串不可变性提出了几个关键实践点:
- 字符串不能通过索引赋值修改单个字符。
- 字符串拼接会创建新字符串。
lower()、upper()、strip()、replace()等方法不会修改原字符串。- 如果要保留操作结果,必须把返回的新字符串赋给变量。
- 理解不可变性有助于正确使用字符串方法和避免误解。
与 01_Datatypes 的关系
summaries/01_Datatypes 将不可变性扩展到数据结构层面,重点展示了元组的行为:
- 元组用于把多个相关值打包成一个整体。
- 元组内容有序,可通过索引访问。
- 元组内容不能原地修改。
- 若要改变元组中的某个字段,需要构造新元组。
- 元组常用于表示简单记录,例如股票持仓中的
(name, shares, price)。 - 字典则提供了可变的替代方案,可以通过键直接修改字段。
这一对比帮助理解:不可变性不仅是语言限制,也是一种数据建模信号。使用元组往往意味着“这是一个固定结构的记录”;使用字典则通常意味着“这些字段可能需要按名称访问和修改”。
与 07_Objects 的关系
summaries/07_Objects 为不可变对象提供了更通用的对象模型背景:
- Python 中一切值都是对象。
- 变量名只是对象引用,不是固定内存位置。
- 赋值不会复制对象,只会复制引用。
- 多个名字可以引用同一个对象。
- 可变对象被共享时,原地修改会影响所有引用。
- 不可变对象不能被原地修改,因此共享引用更安全。
is比较对象身份,==比较对象值。- 浅拷贝和深拷贝主要影响包含可变对象的复合结构。
这些内容说明,不可变性不是孤立规则,而是 Python 引用语义、对象身份、赋值行为和内存管理共同作用下的一部分。
相关概念
- summaries/04_Strings:字符串章节总结,直接展示字符串不可变性。
- summaries/01_Datatypes:通过元组和字典对比展示不可变与可变数据结构。
- summaries/05_Lists:列表是典型可变序列,可与字符串和元组对比。
- summaries/00_Overview:课程概览中涉及 Python 基础数据模型。
- summaries/02_Containers:容器类型与对象组织方式。
- summaries/04_Sequences:序列类型中的字符串、列表、元组对比。
- summaries/07_Objects:解释赋值、引用、对象身份、浅拷贝、深拷贝和类型。
- Python对象模型:从对象、引用、身份和类型理解 Python 值。
- 可变性与引用:解释可变对象共享引用时的副作用。
- 拷贝语义:浅拷贝、深拷贝与嵌套对象共享。
- Python字符串:字符串是最常见的不可变序列之一。
- Python字符串方法:字符串方法通常返回新字符串。
- 元组:元组是典型不可变序列,常用于固定结构记录。
- 字典:字典是可变键值映射,可与元组形成对比。
- concepts/变量绑定:解释变量重新赋值与对象修改的区别。
- Python序列:字符串、列表、元组等序列类型的行为对比。
- 列表:典型可变序列,可与元组和字符串对比。
- Python数据类型:基本类型及其可变性差异。
- Python数据结构:不同数据结构在组织和修改数据时的语义差异。
- Unicode与编码:字符串内容的字符表示与编码背景。
- Python文本处理:不可变字符串在文本处理中的实践影响。
总结
Python 不可变对象的核心是:对象本身不能被原地改变。字符串和元组都是典型例子。对字符串进行拼接、替换、大小写转换、去除空白等操作时,Python 会返回新字符串;对元组中某个字段进行“修改”时,也必须创建新元组。
结合 summaries/07_Objects 可以更准确地理解这一点:变量只是名字,赋值只是引用绑定,不会复制或覆盖对象。变量可以重新绑定到新对象,但原对象没有被修改。不可变对象因此在共享引用时更安全,也更适合作为字典键、集合元素和固定结构记录。理解不可变性是正确掌握 summaries/04_Strings 中字符串操作、summaries/01_Datatypes 中元组记录,以及 变量绑定、Python对象模型 的基础。