一文读懂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. 深拷贝:递归复制整个对象树,创建完全独立的副本

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

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

相关推荐
Tz一号4 分钟前
前端 git规范-不同软件(GitHub、Sourcetree、WebStorm)、命令行合并方式下增加 --no-ff的方法
前端·git·github
冬冬小圆帽22 分钟前
防止手机验证码被刷:React + TypeScript 与 Node.js + Express 的全面防御策略
前端·后端·react.js·typescript
陈明勇34 分钟前
chromem-go:Go 语言 RAG 应用的高效轻量级向量数据库
后端·go
多多*35 分钟前
牛客周赛84 题解 Java ABCDEFG AK实录
数据库·windows·macos·github·objective-c·mybatis·cocoa
掘金詹姆斯1 小时前
从Guava缓存源码提炼业务开发心法:Get方法暗藏的12个高并发设计哲学
后端
零零壹111 小时前
理解Akamai EdgeGrid认证在REST API中的应用
前端·后端
uhakadotcom1 小时前
DataWorks邮件外发完全指南:从零开始实现数据自动推送(2025最新实践)
后端·面试·github
徐小夕1 小时前
JSCAD:一款JavaScript驱动的开源3D设计神器
前端·javascript·github
小兵张健1 小时前
要价 3k 的应届生,何去何从?
java·面试
qq_5470261791 小时前
Spring Boot 实现多数据源配置
java·spring boot·后端