一文读懂copy与deepcopy的区别与实战应用

什么是Python中的拷贝?

在Python中,变量赋值实际上是创建了对象的引用,而非复制对象本身。当我们需要复制对象时,有两种方式:浅拷贝(shallow copy)深拷贝(deep copy)

浅拷贝与深拷贝的基本概念

  • 浅拷贝:只复制对象的第一层,内部嵌套的可变对象仍然是引用关系
  • 深拷贝:递归复制整个对象,包括所有嵌套的对象,创建完全独立的副本

浅拷贝详解

Python提供了多种创建浅拷贝的方式:

ini 复制代码
python
import copy

# 方式1:使用copy模块的copy()函数
原列表 = [1, 2, [3, 4]]
浅拷贝1 = copy.copy(原列表)

# 方式2:使用列表的copy()方法
浅拷贝2 = 原列表.copy()

# 方式3:使用切片操作
浅拷贝3 = 原列表[:]

# 方式4:使用list()构造函数
浅拷贝4 = list(原列表)

# 字典的浅拷贝
原字典 = {'a': 1, 'b': [2, 3]}
浅拷贝字典 = 原字典.copy()  # 或者 copy.copy(原字典)

浅拷贝的特点:修改嵌套对象会影响原对象

scss 复制代码
python
import copy

# 浅拷贝示例
原列表 = [[1, 2], [3, 4], 5]
浅拷贝 = copy.copy(原列表)

# 修改浅拷贝中的嵌套列表
浅拷贝[0][0] = 999

print(f"修改后的浅拷贝:{浅拷贝}")  # [[999, 2], [3, 4], 5]
print(f"原列表:{原列表}")         # [[999, 2], [3, 4], 5] - 原列表也被修改了!

# 但修改浅拷贝的第一层不会影响原列表
浅拷贝[2] = 888
print(f"再次修改后的浅拷贝:{浅拷贝}")  # [[999, 2], [3, 4], 888]
print(f"原列表:{原列表}")             # [[999, 2], [3, 4], 5] - 原列表不受影响

深拷贝详解

深拷贝使用copy.deepcopy()函数创建:

scss 复制代码
python
import copy

# 深拷贝示例
原列表 = [[1, 2], [3, 4], 5]
深拷贝 = copy.deepcopy(原列表)

# 修改深拷贝中的嵌套列表
深拷贝[0][0] = 999

print(f"修改后的深拷贝:{深拷贝}")  # [[999, 2], [3, 4], 5]
print(f"原列表:{原列表}")         # [[1, 2], [3, 4], 5] - 原列表不受影响!

深拷贝处理循环引用

深拷贝能够智能处理循环引用的情况:

python 复制代码
python
import copy

# 创建循环引用
循环列表 = [1, 2, 3]
循环列表.append(循环列表)  # 列表引用自身

# 深拷贝可以正确处理循环引用
深拷贝循环 = copy.deepcopy(循环列表)

print(f"循环列表:{id(循环列表)}")
print(f"循环列表中的自引用:{id(循环列表[3])}")
print(f"深拷贝:{id(深拷贝循环)}")
print(f"深拷贝中的自引用:{id(深拷贝循环[3])}")

性能对比

深拷贝与浅拷贝的性能差异:

python 复制代码
python
import copy
import time

# 创建大型嵌套列表
大列表 = [[i for i in range(1000)] for _ in range(100)]

# 测量浅拷贝性能
开始时间 = time.time()
浅拷贝 = copy.copy(大列表)
结束时间 = time.time()
print(f"浅拷贝耗时: {(结束时间-开始时间)*1000:.2f}毫秒")  # 通常<1毫秒

# 测量深拷贝性能
开始时间 = time.time()
深拷贝 = copy.deepcopy(大列表)
结束时间 = time.time()
print(f"深拷贝耗时: {(结束时间-开始时间)*1000:.2f}毫秒")  # 通常在10-50毫秒之间

实际应用场景

1. 函数参数中避免副作用

lua 复制代码
python
import copy

def 安全处理(数据列表):
    # 创建深拷贝避免修改原始数据
    工作副本 = copy.deepcopy(数据列表)
    # 对工作副本进行操作
    for i in range(len(工作副本)):
        if isinstance(工作副本[i], list):
            工作副本[i][0] *= 2
    return 工作副本

原始数据 = [[1, 2], [3, 4]]
处理后数据 = 安全处理(原始数据)
print(f"原始数据: {原始数据}")  # [[1, 2], [3, 4]] - 不变
print(f"处理后数据: {处理后数据}")  # [[2, 2], [6, 4]]

2. 缓存中间状态

python 复制代码
python
import copy

class 游戏状态:
    def __init__(self):
        self.玩家位置 = [0, 0]
        self.分数 = 0
        self.收集物品 = []
    
    def 保存检查点(self):
        return copy.deepcopy(self)
    
    def 从检查点恢复(self, 检查点):
        self.玩家位置 = 检查点.玩家位置
        self.分数 = 检查点.分数
        self.收集物品 = 检查点.收集物品

# 使用示例
游戏 = 游戏状态()
游戏.分数 = 100
游戏.收集物品 = ["钥匙", "宝石"]

# 保存检查点
检查点 = 游戏.保存检查点()

# 继续游戏
游戏.分数 = 150
游戏.收集物品.append("剑")

print(f"当前状态: 分数={游戏.分数}, 物品={游戏.收集物品}")
# 当前状态: 分数=150, 物品=['钥匙', '宝石', '剑']

# 从检查点恢复
游戏.从检查点恢复(检查点)
print(f"恢复后状态: 分数={游戏.分数}, 物品={游戏.收集物品}")
# 恢复后状态: 分数=100, 物品=['钥匙', '宝石']

总结

  1. 浅拷贝:只复制对象的第一层,内部嵌套的可变对象共享引用

    • 适合:对象结构简单或只需修改第一层时
    • 性能:快速,内存占用少
  2. 深拷贝:递归复制整个对象树,创建完全独立的副本

    • 适合:需要避免任何副作用,或对象有复杂嵌套结构时
    • 性能:较慢,内存占用较多

根据实际需求,选择合适的拷贝方式,可以有效避免意外的数据修改问题,提高代码的健壮性。

相关推荐
yanlele43 分钟前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
ai小鬼头1 小时前
Ollama+OpenWeb最新版0.42+0.3.35一键安装教程,轻松搞定AI模型部署
后端·架构·github
萧曵 丶2 小时前
Rust 所有权系统:深入浅出指南
开发语言·后端·rust
老任与码2 小时前
Spring AI Alibaba(1)——基本使用
java·人工智能·后端·springaialibaba
小兵张健2 小时前
武汉拿下 23k offer 经历
java·面试·ai编程
华子w9089258593 小时前
基于 SpringBoot+VueJS 的农产品研究报告管理系统设计与实现
vue.js·spring boot·后端
爱莉希雅&&&3 小时前
技术面试题,HR面试题
开发语言·学习·面试
天天扭码3 小时前
《很全面的前端面试题》——HTML篇
前端·面试·html
星辰离彬3 小时前
Java 与 MySQL 性能优化:Java应用中MySQL慢SQL诊断与优化实战
java·后端·sql·mysql·性能优化
GetcharZp4 小时前
彻底告别数据焦虑!这款开源神器 RustDesk,让你自建一个比向日葵、ToDesk 更安全的远程桌面
后端·rust