在Python中处理数据,尤其是可变对象(如列表、字典)时,理解赋值、浅拷贝和深拷贝之间的区别至关重要。错误地使用拷贝机制可能导致数据意外共享和修改,引发难以察觉的bug。本篇将深入辨析这三者,并探讨何时应使用Python的 copy 模块。

一、赋值操作:仅仅是“贴标签”

<a name="1-赋值操作-只是贴标签不是拷贝"></a>

当你执行 b = a 这样的赋值语句时,其行为取决于 a 指向的对象类型:

  • 如果 a 指向的是不可变对象 (如数字、字符串、元组),b 会得到这个值。由于不可变对象的值不能改变,通常不会因共享而产生问题。
  • 如果 a 指向的是可变对象 (如列表、字典、集合),b = a 并不会创建 a 的一个新副本。它只是创建了一个新的名称(标签)b,这个新标签与 a 一样,都指向内存中同一个对象

通俗理解: “你给同一个钱包(内存中的对象)贴上了两个名字标签(ab)。通过任何一个名字标签往钱包里放钱或拿钱,钱包里的钱都会变,另一个名字标签看到的也是这个变化后的钱包。”

a = [1, 2, [30, 40]] # a 是一个包含列表的列表 (可变对象)
b = a # b 和 a 指向同一个列表对象

print(f"a: {a}, id(a): {id(a)}")
print(f"b: {b}, id(b): {id(b)}")
print(f"a is b: {a is b}") # 输出: True (它们是同一个对象,id相同)

b.append(5) # 通过 b 修改列表
print(f"a after b.append(5): {a}") # 输出: [1, 2, [30, 40], 5] (a也跟着变了)

b[2][0] = 300 # 通过 b 修改嵌套列表的内容
print(f"a after b[2][0]=300: {a}") # 输出: [1, 2, [300, 40], 5] (a的嵌套列表内容也变了)

从上面代码可见,修改 b 会直接影响 a,因为它们指向的是同一个内存地址。

二、浅拷贝 (Shallow Copy):复制顶层,共享内层

<a name="2-浅拷贝-shallow-copy"></a>

2.1 浅拷贝的行为

通俗理解:“就像复印一本书的第一层目录。书的封面和目录本身(顶层对象)是新复印出来的,所以你有了一本新的书(新的顶层容器对象)。但是,如果目录里的条目(元素)指向的是其他共享的书籍(内部的可变对象),那么复印出来的目录条目仍然指向那些原来的、共享的书籍的‘地址’(引用),而不是把那些共享的书籍也重新复印一遍。”

技术上讲,浅拷贝会创建一个新的顶层容器对象。对于容器内部的元素:

  • 如果是基本数据类型(如数字、字符串,它们是不可变的),则复制其值。
  • 如果元素是对其他可变对象的引用(比如列表中的另一个列表,或字典中的一个列表值),则只复制这个“门牌号”(引用),而不是那个门牌号指向的“房子”(实际对象)。
2.2 如何进行浅拷贝
  1. 使用 copy 模块的 copy() 函数: import copy; shallow_copy = copy.copy(original_object)
  2. 对于列表,可以使用切片操作: shallow_copy_list = original_list[:]
  3. 对于某些内置容器类型 (如 list, dict, set),它们有自己的 .copy() 方法: shallow_copy_dict = original_dict.copy()
2.3 浅拷贝效果示例

import copy

original_list = [1, 2, [30, 40]] # 内部元素 [30, 40] 是一个可变对象
shallow_copied_list = copy.copy(original_list)
# shallow_copied_list = original_list[:] # 对于列表,效果相同
# shallow_copied_list = original_list.copy() # 对于列表,效果相同

print(f"original_list: {original_list}, id: {id(original_list)}")
print(f"shallow_copied_list: {shallow_copied_list}, id: {id(shallow_copied_list)}")
print(f"original_list is shallow_copied_list: {original_list is shallow_copied_list}") # False (顶层对象不同)
print(f"original_list[2] is shallow_copied_list[2]: {original_list[2] is shallow_copied_list[2]}") # True (内部嵌套的列表是同一个对象!)

# 修改浅拷贝对象的顶层结构
shallow_copied_list.append(5) # 在浅拷贝列表末尾添加元素
print(f"Original list after shallow_copy.append(5): {original_list}") # [1, 2, [30, 40]] (原列表顶层不变)
print(f"Shallow copied list: {shallow_copied_list}") # [1, 2, [30, 40], 5]

# 修改浅拷贝对象内部共享的可变子对象的内容
shallow_copied_list[2][0] = 300 # 修改浅拷贝列表中那个被共享的嵌套列表的元素
print(f"Original list after shallow_copy[2][0]=300: {original_list}") # [1, 2, [300, 40]] (原列表也受影响了!)
print(f"Shallow copied list: {shallow_copied_list}") # [1, 2, [300, 40], 5]

可以看到,修改浅拷贝对象的顶层(如 append(5))不影响原对象。但如果修改了浅拷贝对象内部共享的可变子对象(如嵌套列表 [300, 40]),原对象中对应的共享子对象也会随之改变。

三、深拷贝 (Deep Copy):彻底的“克隆”

<a name="3-深拷贝-deep-copy"></a>

3.1 深拷贝的行为

通俗理解:“这次是彻底的‘克隆’,不仅书本身是全新的,书里面引用的所有其他书籍(内部可变对象),以及那些书籍里面再引用的书籍……所有的一切都会被递归地重新复制一份全新的。新书和旧书,以及它们各自包含的所有层级的内容,都完全独立,互不相干。”

技术上讲,深拷贝会创建一个完全独立的新对象。原对象中包含的所有子对象(以及子对象的子对象,等等)都会被递归地复制。修改深拷贝对象或其任何部分的内部内容,都不会影响到原对象。

3.2 如何进行深拷贝

必须使用 copy 模块的 deepcopy() 函数:

import copy; deep_copy = copy.deepcopy(original_object)

3.3 深拷贝效果示例

import copy

original_list = [1, 2, [30, 40]]
deep_copied_list = copy.deepcopy(original_list)

print(f"original_list: {original_list}, id: {id(original_list)}")
print(f"deep_copied_list: {deep_copied_list}, id: {id(deep_copied_list)}")
print(f"original_list is deep_copied_list: {original_list is deep_copied_list}") # False (顶层对象不同)
print(f"original_list[2] is deep_copied_list[2]: {original_list[2] is deep_copied_list[2]}") # False (内部嵌套的列表也是新对象了!)

# 修改深拷贝对象的顶层
deep_copied_list.append(5)
print(f"Original list after deep_copy.append(5): {original_list}") # [1, 2, [30, 40]] (原列表顶层不变)
print(f"Deep copied list: {deep_copied_list}") # [1, 2, [30, 40], 5]

# 修改深拷贝对象中内部列表的元素
deep_copied_list[2][0] = 300
print(f"Original list after deep_copy[2][0]=300: {original_list}") # [1, 2, [30, 40]] (原列表完全不受影响!)
print(f"Deep copied list: {deep_copied_list}") # [1, 2, [300, 40], 5]

注意: 深拷贝可能会比浅拷贝慢,因为它需要递归复制所有对象。如果对象图中存在循环引用,copy.deepcopy() 也能正确处理(它会记录已拷贝的对象以避免无限递归)。

四、总结与选择:何时使用何种拷贝?

<a name="4-总结与选择"></a>

  • 赋值 (=): 当你只是想用不同的名字引用内存中的同一个对象时使用。这是引用,不是拷贝。
  • 浅拷贝 (copy.copy(), [:], .copy() 方法):
    • 当你想要一个新的顶层容器,但可以接受内部可变元素被共享时使用。
    • 或者当你知道容器内部元素都是不可变类型时(此时浅拷贝效果等同深拷贝,且更快)。
    • 适用于只修改顶层结构而不希望影响原对象,但要特别注意共享子对象的修改会相互影响。
  • 深拷贝 (copy.deepcopy()):
    • 当你需要一个对象的完全独立副本,确保对副本的任何修改(包括深层嵌套对象的修改)都不会影响原对象时使用。
    • 这是最安全的复制方式,尤其在处理复杂嵌套的可变数据结构时,但可能代价较高(时间和内存)。

五、与Java拷贝机制的对比

<a name="5-vs-java"></a>

  • 对象赋值: Java中的对象赋值 (Object b = a;) 也是引用赋值,行为与Python的 = 对可变对象的赋值类似,ab 指向同一块内存。
  • 浅拷贝: Java中,Object 类的 clone() 方法通常设计为实现浅拷贝(但具体行为取决于类的实现,需要类实现 Cloneable 接口并重写 clone() 方法)。例如,ArrayListclone() 是浅拷贝其元素引用。
  • 深拷贝: Java中实现深拷贝通常没有像Python copy.deepcopy() 这样直接的内置通用函数。一般需要开发者手动编码实现,例如:
    • 递归地为所有成员对象创建新的副本。
    • 利用对象的序列化和反序列化机制(将对象写入字节流再从字节流中读出,可以得到一个全新的对象图,但这要求对象及其所有成员都实现 Serializable 接口)。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐