Python 复制的艺术:深拷贝与浅拷贝全解

部署运行你感兴趣的模型镜像


在 Python 编程中,我们经常需要复制数据。一个常见的场景是:你有一个复杂的数据结构(比如一个包含用户信息的列表),你想创建一个副本进行修改,同时又不希望影响到原始数据。这时候,如果你只是简单地使用 = 赋值,那你可能已经掉进了第一个"坑"。

要真正地复制一个对象,Python 的 copy 模块为我们提供了两种方式:浅拷贝(Shallow Copy)和深拷贝(Deep Copy)。理解这两者的区别,是编写稳健、无副作用代码的关键一步。

一、从"假复制"开始:赋值操作 (=)

在深入拷贝之前,我们必须先弄清楚最常见的误解:赋值不是复制

当你执行 new_list = old_list 时,你并没有创建一个新的列表。你只是创建了一个新的变量名(标签),让它指向了和 old_list 完全相同的那个列表对象。

它们是同一个对象的两个不同名字而已。

# 原始列表
old_list = [1, 2, ['a', 'b']]

# 赋值操作
new_list = old_list

print(f"old_list 的内存地址: {id(old_list)}")
print(f"new_list 的内存地址: {id(new_list)}")

# 修改 new_list
new_list[2].append('c')

print(f"修改后,old_list 的内容: {old_list}")
print(f"修改后,new_list 的内容: {new_list}")

输出:

old_list 的内存地址: 4442784960
new_list 的内存地址: 4442784960
修改后,old_list 的内容: [1, 2, ['a', 'b', 'c']]
修改后,new_list 的内容: [1, 2, ['a', 'b', 'c']]

内存地址完全相同,修改 new_list 会直接影响 old_list,因为它们本质上就是同一个东西。这显然不是我们想要的"副本"。

二、浅拷贝 (Shallow Copy): 只复制"表面"

为了解决上面的问题,copy 模块登场了。首先是浅拷贝 copy.copy()

浅拷贝会创建一个新的顶层对象,但它内部的元素,是原始对象元素的引用(指针)。

可以把它想象成复印一份合同:你得到了一份新的合同文件(新的顶层对象),但如果合同里提到了某个具体的、共享的附件(内部的嵌套对象),两份合同引用的都是同一个附件。

让我们看看代码:

import copy

old_list = [1, 2, ['a', 'b']]

# 进行浅拷贝
shallow_copied_list = copy.copy(old_list)

print(f"--- 内存地址对比 ---")
print(f"old_list: {id(old_list)}")
print(f"shallow_copied_list: {id(shallow_copied_list)}")
print(f"old_list[2] (嵌套列表) 的内存地址: {id(old_list[2])}")
print(f"shallow_copied_list[2] (嵌套列表) 的内存地址: {id(shallow_copied_list[2])}")

print("\n--- 修改数据 ---")
# 修改嵌套列表
shallow_copied_list[2].append('c')

# 修改顶层元素
shallow_copied_list[0] = 99

print(f"修改后,old_list: {old_list}")
print(f"修改后,shallow_copied_list: {shallow_copied_list}")

输出:

--- 内存地址对比 ---
old_list: 4532536512
shallow_copied_list: 4532536640
old_list[2] (嵌套列表) 的内存地址: 4532536448
shallow_copied_list[2] (嵌套列表) 的内存地址: 4532536448

--- 修改数据 ---
修改后,old_list: [1, 2, ['a', 'b', 'c']]
修改后,shallow_copied_list: [99, 2, ['a', 'b', 'c']]

结果分析:

  1. old_listshallow_copied_list 的内存地址不同,说明浅拷贝确实创建了一个新的顶层列表
  2. 但它们内部的嵌套列表 ['a', 'b'] 的地址是相同的。它们共享着同一个嵌套列表对象。
  3. 因此,当我们修改这个共享的嵌套列表时(append('c')),old_listshallow_copied_list 都受到了影响。
  4. 而当我们修改顶层元素 shallow_copied_list[0] = 99 时,由于整数是不可变类型,这相当于给新列表的第一个位置换上了一个新对象,所以 old_list 不受影响。

浅拷贝的结论:它只拷贝了第一层。如果对象里包含了其他可变对象(如列表、字典),拷贝的只是这些可变对象的引用。

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

如果你希望得到一个完完全全、彻彻底底的独立副本,无论数据结构有多深,那么你需要深拷贝 copy.deepcopy()

深拷贝会创建一个新的对象,并递归地复制原始对象中的所有子对象。

继续用合同的比喻:深拷贝不仅复印了合同本身,还把合同里提到的所有附件也全都复印了一份新的。这样,新旧两份合同及其所有附件之间,再无任何关联。

import copy

old_list = [1, 2, ['a', 'b']]

# 进行深拷贝
deep_copied_list = copy.deepcopy(old_list)

print(f"--- 内存地址对比 ---")
print(f"old_list: {id(old_list)}")
print(f"deep_copied_list: {id(deep_copied_list)}")
print(f"old_list[2] (嵌套列表) 的内存地址: {id(old_list[2])}")
print(f"deep_copied_list[2] (嵌套列表) 的内存地址: {id(deep_copied_list[2])}")

print("\n--- 修改数据 ---")
# 修改嵌套列表
deep_copied_list[2].append('c')

print(f"修改后,old_list: {old_list}")
print(f"修改后,deep_copied_list: {deep_copied_list}")

输出:

--- 内存地址对比 ---
old_list: 4420195520
deep_copied_list: 4420195648
old_list[2] (嵌套列表) 的内存地址: 4420195456
deep_copied_list[2] (嵌套列表) 的内存地址: 4420195776

--- 修改数据 ---
修改后,old_list: [1, 2, ['a', 'b']]
修改后,deep_copied_list: [1, 2, ['a', 'b', 'c']]

结果分析:
这次,不仅顶层列表的地址不同,连嵌套列表的地址也不同了deepcopy 为我们创建了一个全新的、独立的嵌套列表。

因此,修改 deep_copied_list 里的任何内容,都丝毫不会影响到 old_list。这才是真正的"克隆"。

四、总结:何时使用哪种拷贝?

操作方式行为何时使用
赋值 (=)创建一个指向相同对象的新别名当你希望多个变量共享和操作同一个对象时。
浅拷贝 (copy.copy)创建新的顶层对象,但共享内部的子对象引用当你的数据结构只有一层,或者你确定不会修改内部的嵌套可变对象时。它比深拷贝快。
深拷贝 (copy.deepcopy)递归地创建所有对象的全新副本当你的数据结构包含嵌套的可变对象(如列表、字典),且你需要一个完全独立的副本时。这是最安全的选择,可以避免意外的副作用。

经验法则:

  • 如果你的数据很简单,比如一个只包含数字和字符串的列表,用浅拷贝就足够了,而且效率更高。
  • 如果你不确定数据的内部结构,或者数据中包含了列表、字典等可变容器,为了避免未来出现难以追踪的 Bug,请毫不犹豫地使用深拷贝

掌握深浅拷贝的区别,能让你在处理复杂数据时更加得心应手,写出更可靠的 Python 代码.

五、进阶:拷贝的更多细节

1. 其他内置容器也需要注意

  • 字典 / 集合:与列表类似,copy.copy() 只复制第一层键值或元素,嵌套结构依旧共享引用。
  • 元组:元组不可变,但如果内部包含可变对象(如列表),浅拷贝仍会共享这些对象。
import copy

t1 = (1, [2, 3])
# 浅拷贝:返回同一元组对象(ID 不变)
shallow = copy.copy(t1)
# 深拷贝:创建新元组并递归复制内部可变对象
deep = copy.deepcopy(t1)

shallow[1].append(4)  # 影响原元组
print(t1)   # (1, [2, 3, 4]) ← 受浅拷贝修改影响

deep[1].append(5)     # 不影响原元组
print(t1)   # (1, [2, 3, 4])
print(deep) # (1, [2, 3, 5]) ← 深拷贝独立

2. list.copy()、切片与 copy.copy()

  • 对列表、字典、集合而言,.copy() 方法或切片 old_list[:]copy.copy() 完全等价,都是浅拷贝:
shallow1 = old_list[:]      # 切片
shallow2 = old_list.copy()  # 内置方法
shallow3 = copy.copy(old_list)

3. 为自定义类定制拷贝行为

如果你的类包含复杂状态,可实现 __copy__ / __deepcopy__ 钩子来自定义拷贝逻辑:

class User:
    def __init__(self, name, settings):
        # 初始化:name 为不可变字符串,settings 为可变字典
        self.name = name
        self.settings = settings

    def __copy__(self):
        # 浅拷贝:复制对象本身,但内部可变字段仍共享
        cls = self.__class__                # 获取当前类引用
        new_obj = cls.__new__(cls)          # 仅分配内存,不调用 __init__
        new_obj.__dict__.update(self.__dict__)  # 复制所有属性引用
        return new_obj

    def __deepcopy__(self, memo):
        # 深拷贝:递归复制所有可变子对象
        import copy
        cls = self.__class__
        new_obj = cls.__new__(cls)          # 创建空对象
        memo[id(self)] = new_obj            # 记录到 memo,处理循环引用
        for k, v in self.__dict__.items():  # 遍历原对象属性
            # 递归 deepcopy 属性值,并写入新对象
            setattr(new_obj, k, copy.deepcopy(v, memo))
        return new_obj

4. 深拷贝如何处理循环引用

copy.deepcopy() 使用内部的 memo 字典记录已复制对象,检测到循环引用时会返回已创建的副本,从而避免无限递归。

5. 不可复制的系统资源

文件句柄、线程锁、网络连接等系统资源无法真正复制,浅拷贝或深拷贝只会共享这些底层对象或抛出异常。如需复制,应在类的 __deepcopy__ 中自行处理,或者显式关闭并重新创建资源。

6. 性能与内存开销

深拷贝会递归创建新对象,消耗显著高于浅拷贝。在大型数据结构或高频调用场景中,建议:

  1. 评估是否真的需要深拷贝。
  2. 尝试"浅拷贝 + 手动复制关键子对象"以减少成本。

7. 官方文档与更多阅读

您可能感兴趣的与本文相关的镜像

Python3.9

Python3.9

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

showyouii

buy me a coffee

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值