Python进阶-深浅拷贝辨析

一、引言

在 Python 编程中,我们经常需要复制对象。但简单的赋值操作往往不能满足我们的需求,因为它只是创建了一个新的引用,而不是真正的对象副本。这时,我们就需要使用浅拷贝深拷贝来创建对象的独立副本。

所谓浅拷贝指的是:copy模块的copy(),深拷贝指的是copy模块的deepcopy()模块

深浅拷贝可以按照以下类型进行分类:

  1. 浅拷贝可变类型
  2. 浅拷贝不可变类型
  3. 深拷贝可变类型
  4. 深拷贝不可变类型

关于可变类和不可变类型的辨析:

在 python 中,strings, tuples, 和 numbers 是不可更改的对象,而 list,dict 等则是可以修改的对象。

  • 不可变类型: 变量赋值 a=5 后再赋值 a=10,这里实际是新生成一个 int 值对象 10,再让 a 指向它,而 5 被丢弃,不是改变 a 的值,相当于新生成了 a。
  • 可变类型: 变量赋值 la=[1,2,3,4] 后再赋值 la[2]=5 则是将 list la 的第三个元素值更改,本身la没有动,只是其内部的一部分值被修改了。

python 函数的参数传递:

  • **不可变类型:**类似 C++ 的值传递,如整数、字符串、元组。如 fun(a),传递的只是 a 的值,没有影响 a 对象本身。如果在 fun(a) 内部修改 a 的值,则是新生成一个 a 的对象。
  • **可变类型:**类似 C++ 的引用传递,如 列表,字典。如 fun(la),则是将 la 真正的传过去,修改后 fun 外部的 la 也会受影响

python 中一切都是对象,严格意义我们不能说值传递还是引用传递,我们应该说传不可变对象和传可变对象。

深浅拷贝主要是针对可变类型来讲的,如果是针对于不可变类型, 则用法和普通赋值一样.

二、赋值操作(Assignment)

在讲解深浅拷贝之前,我们首先需要理解 Python 中的赋值操作,因为它是最基础的对象"复制"方式,但往往也是最容易产生误解的。

2.1 定义

赋值操作(=)只是创建了一个新的变量名,该变量名与原变量名指向同一个内存地址中的对象。换句话说,赋值操作并没有创建新的对象,只是增加了一个引用。

2.2 代码示例

复制代码
# 定义一个列表对象
a = [1, 2, [3, 4]]
# 赋值操作
b = a

print("a 的内存地址:", id(a))
print("b 的内存地址:", id(b))  # 与 a 相同
print("a == b:", a == b)        # True,值相同
print("a is b:", a is b)        # True,是同一个对象

# 修改 a 中的元素
a[0] = 100
# 修改 a 中嵌套的列表
a[2][0] = 300

print("修改后的 a:", a)  # [100, 2, [300, 4]]
print("修改后的 b:", b)  # [100, 2, [300, 4]],b 也跟着变了

2.3 注意事项

  • 赋值操作后,两个变量共享同一个对象。
  • 对其中一个变量所指向的对象进行修改(无论是修改顶层元素还是嵌套元素),都会影响到另一个变量。

三、浅拷贝(Shallow Copy)

浅拷贝是一种创建新对象的方式,但它只复制对象的"顶层"结构。对于对象中的嵌套元素(如子列表、子字典等),浅拷贝只复制它们的引用,而不复制嵌套对象本身。

3.1 定义

浅拷贝创建一个新的容器对象(如一个新的列表),但容器里面的元素仍然是原对象中元素的引用。

3.2 常见实现方式

在 Python 中,实现浅拷贝的常见方式有:

  1. 使用 copy 模块的 copy() 函数。
  2. 使用列表的切片操作 list[:]
  3. 使用列表、字典等内置数据结构的 copy() 方法。
  4. 使用工厂函数,如 list()dict()

3.3 代码示例

复制代码
import copy

# 定义一个包含嵌套列表的列表
a = [1, 2, [3, 4]]

# 方式 1: 使用 copy.copy()
b = copy.copy(a)
# 方式 2: 使用切片
# b = a[:]
# 方式 3: 使用 list.copy()
# b = a.copy()
# 方式 4: 使用 list() 工厂函数
# b = list(a)

print("a 的内存地址:", id(a))
print("b 的内存地址:", id(b))        # 与 a 不同,说明 b 是新对象
print("a == b:", a == b)              # True,值相同
print("a is b:", a is b)              # False,不是同一个对象

print("a[2] 的内存地址:", id(a[2]))
print("b[2] 的内存地址:", id(b[2]))  # 与 a[2] 相同!说明嵌套列表没有被复制

# 修改 a 的顶层元素(不可变元素)
a[0] = 100
print("修改 a[0] 后的 a:", a)  # [100, 2, [3, 4]]
print("修改 a[0] 后的 b:", b)  # [1, 2, [3, 4]],b 不受影响

# 修改 a 中的嵌套列表(可变元素)
a[2][0] = 300
print("修改 a[2][0] 后的 a:", a)  # [100, 2, [300, 4]]
print("修改 a[2][0] 后的 b:", b)  # [1, 2, [300, 4]],b 也跟着变了!

3.4 工作原理

  1. 创建一个新的容器对象(如列表 b)。
  2. 将原对象(列表 a)中的元素引用复制到新容器中。
  3. 如果原对象中的元素是不可变对象(如数字、字符串、元组),修改原对象的顶层元素不会影响浅拷贝对象。
  4. 如果原对象中的元素是可变对象(如列表、字典),因为复制的是引用,所以修改原对象中的嵌套可变对象,浅拷贝对象中的对应元素也会改变。

四、深拷贝(Deep Copy)

深拷贝是一种完全复制对象的方式。它不仅复制对象的顶层结构,还会递归地复制对象中所有的嵌套元素。

4.1 定义

深拷贝创建一个新的容器对象,并且递归地将原对象中的所有元素(包括嵌套再深的对象)都复制一份。原对象和拷贝对象之间完全独立,互不影响。

4.2 常见实现方式

在 Python 中,实现深拷贝主要使用 copy 模块的 deepcopy() 函数。

4.3 代码示例

复制代码
import copy

# 定义一个包含嵌套列表的列表
a = [1, 2, [3, 4]]

# 使用 copy.deepcopy() 进行深拷贝
b = copy.deepcopy(a)

print("a 的内存地址:", id(a))
print("b 的内存地址:", id(b))        # 与 a 不同
print("a[2] 的内存地址:", id(a[2]))
print("b[2] 的内存地址:", id(b[2]))  # 与 a[2] 也不同!嵌套列表也被复制了

# 修改 a 的顶层元素
a[0] = 100
print("修改 a[0] 后的 a:", a)  # [100, 2, [3, 4]]
print("修改 a[0] 后的 b:", b)  # [1, 2, [3, 4]],b 不受影响

# 修改 a 中的嵌套列表
a[2][0] = 300
print("修改 a[2][0] 后的 a:", a)  # [100, 2, [300, 4]]
print("修改 a[2][0] 后的 b:", b)  # [1, 2, [3, 4]],b 仍然不受影响!

4.4 工作原理

  1. 创建一个新的容器对象。
  2. 递归地遍历原对象中的每一个元素。
  3. 如果是不可变对象,则复制其引用(或直接复用,因为不可变)。
  4. 如果是可变对象,则在内存中创建一个新的该对象,并将新对象的引用放入新容器中。
  5. 结果是,原对象和深拷贝对象在内存中是完全独立的两棵树。

五、赋值、浅拷贝、深拷贝区别对比

|----------------|--------------|---------------------|-------------------------|
| 特性 | 赋值 (=) | 浅拷贝 (copy.copy()) | 深拷贝 (copy.deepcopy()) |
| 新对象创建 | 不创建新对象,只增加引用 | 创建新对象(仅顶层容器) | 创建新对象(完全独立) |
| 非容器元素(如数字) | 指向同一对象 | 复制引用(效果类似赋值) | 复制引用(效果类似赋值) |
| 容器元素(如子列表) | 指向同一对象 | 复制引用(共享子对象) | 递归复制(独立子对象) |
| 内存开销 | 最小 | 较小 | 较大(取决于对象复杂度) |
| 修改原对象顶层元素 | 影响对方 | 不影响对方 | 不影响对方 |
| 修改原对象嵌套元素 | 影响对方 | 影响对方 | 不影响对方 |


六、使用场景建议

  1. 赋值操作 ( =)
    • 当你只是想给同一个对象起个别名,或者不需要修改对象时使用。
    • 例如:user = current_user(只是为了代码可读性)。
  1. 浅拷贝 ( copy.copy())
    • 当你想复制一个容器,但容器内的元素都是不可变对象(如数字、字符串、元组)时使用。
    • 当你想复制一个容器,且你确定不会修改其中的嵌套可变对象时使用。
    • 例如:复制一个简单的配置列表 config_copy = config[:]
  1. 深拷贝 ( copy.deepcopy())
    • 当你需要一个完全独立的副本,且对象中包含嵌套的可变对象时,必须使用深拷贝。
    • 例如:在处理复杂的数据结构(如图、树、JSON数据)时,为了防止修改副本影响原始数据。

七、常见陷阱与注意事项

7.1 不可变对象的"特殊性"

对于不可变对象(如 tuplestrint),浅拷贝有时并不会真正创建新对象,因为 Python 为了优化性能,会复用不可变对象。但这通常不会影响程序逻辑,因为对象本身不可修改。

复制代码
import copy
a = (1, 2, 3)
b = copy.copy(a)
print(a is b)  # 可能是 True,因为元组不可变,Python 优化了

7.2 循环引用

deepcopy 会智能地处理对象中的循环引用(即对象 A 引用 B,B 又引用 A),防止无限递归。

复制代码
import copy
a = [1, 2]
a.append(a)  # 自己引用自己,形成循环
print(a)     # [1, 2, [...]]

b = copy.deepcopy(a)
print(b)     # [1, 2, [...]],深拷贝成功,不会报错

7.3 自定义对象

对于自定义的类实例,深浅拷贝的行为同样适用。如果需要控制自定义对象的拷贝行为,可以重写 __copy__()__deepcopy__() 方法。


八、总结

  • 赋值:不是拷贝,只是引用计数加一。
  • 浅拷贝:"表皮"复制,容器是新的,但里面的东西还是旧的引用。
  • 深拷贝:"彻底"复制,容器和里面的所有东西都是新的。

理解了这三者的区别,你就能在处理复杂数据结构时避免很多难以察觉的 Bug。记住口诀:"赋值靠引用,浅拷贝只一层,深拷贝全递归"


相关推荐
时寒的笔记2 小时前
js逆向7_案例惠nong网
android·开发语言·javascript
Thomas.Sir2 小时前
重构诊疗效率与精准度之【AI 赋能临床诊断与辅助决策从理论到实战】
人工智能·python·ai·医疗·诊断
V胡桃夹子2 小时前
pyenv-win 完整安装+使用手册
python·pyenv
Evand J2 小时前
【MATLAB例程】基于低精度IMU、GNSS的UAV初始航向(三维角度)校准的仿真,包含卡尔曼滤波、惯导解算与校正
开发语言·matlab·gnss·imu·卡尔曼滤波
ego.iblacat2 小时前
Python 连接 MySQL 数据库
数据库·python·mysql
feng_you_ying_li2 小时前
c++之哈希表的介绍与实现
开发语言·c++·散列表
网域小星球2 小时前
C 语言从 0 入门(十四)|文件操作:读写文本、保存数据持久化
c语言·开发语言·文件操作·fopen·fprintf
网域小星球2 小时前
C 语言从 0 入门(七)|字符数组与字符串完整精讲|VS2022 高质量实战
c语言·开发语言·字符串·vs2022·字符数组
Jia ming2 小时前
C语言实现日期天数计算
c语言·开发语言·算法