7.2 KiB
sources, brief
| sources | brief | |
|---|---|---|
|
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 的拷贝语义。该文档强调:变量只是名字,赋值只是引用绑定;理解这一点是避免共享可变对象副作用的基础。
相关页面
- summaries/07_Objects
- Python对象模型
- 可变性与引用
- 一等对象