Files

7.2 KiB
Raw Permalink Blame History

sources, brief
sources brief
summaries/07_Objects.md
Python 拷贝语义说明赋值、浅拷贝与深拷贝如何处理对象引用。

Python 拷贝语义

本页边界

本页是赋值、别名、浅拷贝和深拷贝的总览。若只需要比较浅拷贝和深拷贝的行为,读 concepts/浅拷贝与深拷贝;若要理解为什么赋值不复制对象,读 concepts/变量绑定;若要理解可变对象为什么有共享副作用,读 concepts/Python-可变对象。

Python 拷贝语义描述的是:当一个对象被赋值、放入容器、复制或传递时,Python 究竟是在复制对象本身,还是只是在复制对象的引用。这个概念是理解 summaries/07_Objects 中对象模型、可变对象共享和数据修改副作用的关键。

核心原则:赋值不是复制

在 Python 中,赋值操作不会创建对象副本。赋值只是让一个名字绑定到已有对象,或者让某个容器位置保存对对象的引用。

常见赋值形式包括:

a = value
s[n] = value
s.append(value)
d['key'] = value

这些操作都只是复制引用,而不是复制 value 本身。

例如:

a = [1, 2, 3]
b = a

此时 a 和 b 指向同一个列表对象。修改其中一个名字所指向的列表,会影响另一个名字看到的结果:

a.append(999)

print(a)  # [1, 2, 3, 999]
print(b)  # [1, 2, 3, 999]

这并不是 b 被同步更新了,而是 a 和 b 本来就是同一个对象的两个引用。

相关概念:Python对象模型、可变性与引用。

变量是名字,不是内存位置

Python 中变量名不是固定的内存槽,而是指向对象的名字。重新赋值不会覆盖旧对象,只会让变量名改为引用另一个对象。

a = [1, 2, 3]
b = a
a = [4, 5, 6]

print(a)  # [4, 5, 6]
print(b)  # [1, 2, 3]

这里 a = [4, 5, 6] 并没有修改原来的 [1, 2, 3]。它只是让 a 绑定到一个新的列表对象,而 b 仍然引用旧列表。

因此,理解拷贝语义时要区分两件事:

  • 重新绑定名字:让变量名指向另一个对象。
  • 原地修改对象:改变对象本身的内容。

例如:

a = [1, 2, 3]
b = a

# 原地修改:影响所有共享引用
a.append(4)

# 重新绑定:只改变 a 指向哪里
a = [10, 20]

对象身份与共享引用

判断两个名字是否引用同一个对象,可以使用 is:

a = [1, 2, 3]
b = a

print(a is b)  # True

如果两个对象内容相同但不是同一个对象,is 为 False,而 == 可以为 True:

a = [1, 2, 3]
c = [1, 2, 3]

print(a is c)  # False
print(a == c)  # True

在拷贝语义中:

  • is 用来判断是否是同一个对象。
  • == 用来判断值是否相等。

通常业务逻辑中应该优先使用 ==,只有在确实关心对象身份时才使用 is。

浅拷贝

浅拷贝会创建一个新的外层容器,但不会递归复制容器内部的对象。内部元素仍然是原对象的引用。

例如:

a = [2, 3, [100, 101], 4]
b = list(a)

print(a is b)  # False

a 和 b 是两个不同的外层列表。但是它们的第三个元素,也就是内部列表 [100, 101],仍然是同一个对象:

a[2].append(102)

print(b[2])          # [100, 101, 102]
print(a[2] is b[2])  # True

这说明浅拷贝只复制了“第一层结构”。如果元素本身是可变对象,例如列表、字典、集合或自定义对象,它们仍然可能被多个容器共享。

常见浅拷贝方式包括:

b = list(a)
b = a.copy()
b = a[:]
d2 = dict(d)
d2 = d.copy()

浅拷贝适合以下场景:

  • 只需要一个新的外层列表或字典。
  • 内部元素是不可变对象,例如数字、字符串、元组。
  • 可以接受内部可变对象继续共享。

深拷贝

深拷贝会复制对象本身以及它包含的嵌套对象。可以使用标准库 copy 模块中的 deepcopy():

import copy

a = [2, 3, [100, 101], 4]
b = copy.deepcopy(a)

a[2].append(102)

print(b[2])          # [100, 101]
print(a[2] is b[2])  # False

此时 a 和 b 的外层列表不同,内部列表也不同。修改 a 的内部列表不会影响 b。

深拷贝适合以下场景:

  • 嵌套数据结构中包含可变对象。
  • 需要完全独立的数据副本。
  • 修改副本时不能影响原对象。

但深拷贝也有代价:

  • 可能消耗更多内存。
  • 可能更慢。
  • 对复杂对象、循环引用、自定义类时行为可能更复杂。

因此,深拷贝不是默认选择,应在确实需要隔离嵌套对象时使用。

浅拷贝与深拷贝的区别

操作 是否创建新外层对象 是否复制内部对象 典型结果
赋值 否 否 新名字引用同一对象
浅拷贝 是 否 外层独立,内部共享
深拷贝 是 是 外层和内部都尽量独立

示意理解:

# 赋值:a 和 b 是同一个列表
b = a

# 浅拷贝:b 是新列表,但元素仍可能共享
b = list(a)

# 深拷贝:b 是递归复制出的新结构
b = copy.deepcopy(a)

与可变性之间的关系

拷贝语义最容易在可变对象上引发问题。列表、字典、集合等对象可以被原地修改,因此多个引用共享同一个可变对象时,一个地方的修改会在其他地方显现。

a = []
b = a
b.append('item')

print(a)  # ['item']

如果对象是不可变的,例如整数、浮点数、字符串,则不存在原地修改内容的问题:

x = 10
y = x
x = 20

print(y)  # 10

这里 x = 20 是重新绑定,而不是修改整数对象 10。

这也是为什么 summaries/07_Objects 提到,基础类型如 int、float、str 被设计为不可变对象可以减少共享引用带来的意外风险。

相关概念:可变性与引用。

常见陷阱

1. 以为赋值创建了副本

a = [1, 2, 3]
b = a
b.append(4)

print(a)  # [1, 2, 3, 4]

如果希望 b 是独立列表,应显式复制:

b = list(a)

2. 以为浅拷贝复制了所有内容

a = [[1, 2], [3, 4]]
b = list(a)

b[0].append(99)
print(a)  # [[1, 2, 99], [3, 4]]

外层列表已复制,但内部列表仍共享。

3. 比较对象时误用 is

a = [1, 2, 3]
b = [1, 2, 3]

print(a is b)  # False
print(a == b)  # True

如果关心内容是否相同,应使用 ==。

实践建议

  • 默认记住:赋值不复制对象。
  • 如果只需要新的外层容器,使用浅拷贝。
  • 如果需要完全隔离嵌套结构,使用 copy.deepcopy()。
  • 对可变对象尤其小心,因为共享引用会导致原地修改的副作用。
  • 使用 is 检查对象身份,使用 == 检查值相等。
  • 不要为了“安全”盲目深拷贝,应根据数据结构和修改需求选择合适方式。

在源文档中的位置

summaries/07_Objects 通过列表赋值、列表嵌套、浅拷贝和深拷贝示例说明了 Python 的拷贝语义。该文档强调:变量只是名字,赋值只是引用绑定;理解这一点是避免共享可变对象副作用的基础。

相关页面