Files

21 KiB
Raw Permalink Blame History

sources, brief
sources brief
summaries/07_Objects.md
summaries/04_Sequences.md
summaries/02_Containers.md
summaries/01_Datatypes.md
summaries/00_Overview.md
summaries/05_Lists.md
summaries/04_Strings.md
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',而是:

  1. 表达式 symbols + ',GOOG' 创建了一个新字符串。
  2. 变量名 symbols 重新绑定到这个新字符串。
  3. 原来的字符串如果没有其他引用,之后会被垃圾回收。

字符串方法也不会原地修改

字符串的各种方法都会返回新字符串,而不是修改原字符串。

例如:

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”,但实际发生的是:

  1. 读取旧元组中的股票代码和价格。
  2. 用新的股数 75 构造一个全新的元组。
  3. 变量名 s 从旧元组重新绑定到新元组。
  4. 旧元组本身没有被修改。

这与字符串拼接、字符串方法返回新字符串的机制一致,核心都是变量重新绑定,而不是对象原地变化。

元组作为不可变记录

元组常用于表示一个由多个字段组成的单一对象,类似数据库表中的一行:

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对象模型 的基础。