Python深拷贝与浅拷贝:彻底搞懂引用与复制的本质
·
作为Python学习者,你是否遇到过修改一个列表,另一个列表也跟着变的"灵异事件"?这背后其实是Python的引用机制和拷贝原理在起作用。今天我们就从底层原理出发,结合代码实例,彻底搞懂Python中的深拷贝与浅拷贝,让你在实际开发中不再踩坑。
文章目录
一、先搞懂:Python的赋值到底做了什么
在讲拷贝之前,我们必须先理解Python中最基础的赋值操作。很多人误以为赋值是"复制一个新对象",但实际上完全不是这样。
1.1 赋值的本质:引用传递
# 定义一个嵌套列表
a = [1, 2, 3, [1, 2, 3, [5, 6]]]
# 执行赋值操作
b = a
# 查看两个变量的内存地址
print(id(a)) # 输出:140709324567816
print(id(b)) # 输出:140709324567816
代码解释:
b = a并没有创建一个新的列表对象- 它只是让变量
b指向了变量a所指向的同一个内存地址 - 此时
a和b就像两个不同的门牌号,指向了同一间房子
1.2 赋值操作的后果
# 修改a中的元素
a[0] = 100
print(b[0]) # 输出:100
# 修改b中的嵌套列表
b[3][0] = 99
print(a[3][0]) # 输出:99
代码解释:
- 无论修改
a还是b,另一个变量都会受到影响 - 因为它们本质上操作的是同一个对象
- 这就是为什么我们需要"拷贝"——当我们想要两个独立的对象时,赋值操作就不够用了
二、浅拷贝(Shallow Copy):只复制第一层
浅拷贝是Python中最常用的拷贝方式,它会创建一个新的容器对象,但对于容器内部的元素,仍然使用原来的引用。
2.1 浅拷贝的实现方式
Python中实现浅拷贝有三种常用方法:
- 列表的
copy()方法(推荐) - 切片操作
[:] copy模块的copy()函数
import copy
a = [1, 2, 3, [1, 2, 3, [5, 6]]]
# 三种浅拷贝方式
c1 = a.copy() # 方法1:列表自带的copy方法
c2 = a[:] # 方法2:切片操作
c3 = copy.copy(a) # 方法3:copy模块的copy函数
# 验证:新对象与原对象地址不同
print(id(a) == id(c1)) # 输出:False
代码解释:
- 三种方式都创建了一个新的列表对象
- 所以
a和c1的内存地址不同 - 这是浅拷贝与赋值操作最本质的区别
2.2 浅拷贝的核心特点:只拷贝第一层
这是浅拷贝最关键也是最容易出错的地方。我们来看一个例子:
a = [1, 2, 3, [1, 2, 3, [5, 6]]]
c = a.copy()
# 修改第一层元素
c[1] = 0
print("a:", a) # 输出:a: [1, 2, 3, [1, 2, 3, [5, 6]]]
print("c:", c) # 输出:c: [1, 0, 3, [1, 2, 3, [5, 6]]]
代码解释:
- 修改
c的第一层元素(整数2改为0) - 原列表
a没有受到任何影响 - 因为第一层元素被成功拷贝了,现在是独立的
# 修改嵌套的第二层元素
c[3][0] = 9
print("a:", a) # 输出:a: [1, 2, 3, [9, 2, 3, [5, 6]]]
print("c:", c) # 输出:c: [1, 0, 3, [9, 2, 3, [5, 6]]]
代码解释:
- 修改
c中嵌套列表的元素(1改为9) - 原列表
a中的嵌套列表也跟着变了! - 这就是浅拷贝的局限性:只拷贝了最外层的容器,内部的嵌套对象仍然是共享引用
2.3 浅拷贝的内存模型
原列表a: [1, 2, 3, 引用A]
↓
[1, 2, 3, 引用B]
↓
[5, 6]
浅拷贝后的c: [1, 2, 3, 引用A] # 注意:引用A和a中的引用A指向同一个对象
三、深拷贝(Deep Copy):递归复制所有层级
当我们需要完全独立的对象,无论嵌套多少层都互不影响时,就需要使用深拷贝。
3.1 深拷贝的实现方式
深拷贝只能通过Python内置的 copy 模块的 deepcopy() 函数实现:
import copy
a = [1, 2, 3, [1, 2, 3, [5, 6]]]
# 执行深拷贝
d = copy.deepcopy(a)
# 验证:新对象与原对象地址不同
print(id(a) == id(d)) # 输出:False
3.2 深拷贝的核心特点:完全独立
深拷贝会递归地复制所有层级的对象,创建一个完全独立的副本。无论修改哪一层,原对象都不会受到影响。
# 修改第一层元素
d[1] = 0
print("a:", a) # 输出:a: [1, 2, 3, [1, 2, 3, [5, 6]]]
print("d:", d) # 输出:d: [1, 0, 3, [1, 2, 3, [5, 6]]]
# 修改第二层元素
d[3][0] = 9
print("a:", a) # 输出:a: [1, 2, 3, [1, 2, 3, [5, 6]]]
print("d:", d) # 输出:d: [1, 0, 3, [9, 2, 3, [5, 6]]]
# 修改第三层元素
d[3][3][0] = 100
print("a:", a) # 输出:a: [1, 2, 3, [1, 2, 3, [5, 6]]]
print("d:", d) # 输出:d: [1, 0, 3, [9, 2, 3, [100, 6]]]
代码解释:
- 无论修改
d的哪一层元素,原列表a都完全不受影响 - 因为深拷贝递归地复制了所有层级的对象
- 现在
d是一个完全独立的对象,与a没有任何共享引用
3.3 深拷贝的内存模型
原列表a: [1, 2, 3, 引用A]
↓
[1, 2, 3, 引用B]
↓
[5, 6]
深拷贝后的d: [1, 2, 3, 引用A'] # 引用A'指向一个新的列表对象
↓
[1, 2, 3, 引用B'] # 引用B'指向另一个新的列表对象
↓
[5, 6] # 这也是一个新的列表对象
四、浅拷贝 vs 深拷贝:核心对比总结
| 特性 | 赋值操作 | 浅拷贝 | 深拷贝 |
|---|---|---|---|
| 创建新对象 | ❌ 否 | ✅ 是(仅外层) | ✅ 是(所有层级) |
| 第一层修改影响原对象 | ✅ 是 | ❌ 否 | ❌ 否 |
| 嵌套层修改影响原对象 | ✅ 是 | ✅ 是 | ❌ 否 |
| 性能 | 最快 | 较快 | 最慢(递归复制) |
| 内存占用 | 最少 | 较少 | 最多 |
| 适用场景 | 不需要独立对象 | 无嵌套或仅一层嵌套 | 多层嵌套且需要完全独立 |
五、重要注意事项与常见误区
5.1 不可变类型的拷贝问题
# 不可变类型(如整数、字符串、元组)没有copy属性
a = (1, 2, 3)
a.copy() # 报错:AttributeError: 'tuple' object has no attribute 'copy'
# 对不可变类型执行浅拷贝,实际上返回的是原对象的引用
import copy
b = copy.copy(a)
print(id(a) == id(b)) # 输出:True
解释:
- 不可变类型一旦创建就不能修改,所以拷贝它们没有意义
- Python会直接返回原对象的引用,以节省内存和提高性能
5.2 循环引用的处理
深拷贝能够正确处理循环引用的情况,不会导致无限递归:
import copy
# 创建一个循环引用
a = [1, 2]
a.append(a)
# 深拷贝仍然可以正常工作
b = copy.deepcopy(a)
print(b) # 输出:[1, 2, [...]]
5.3 自定义对象的拷贝
对于自定义类的对象,浅拷贝和深拷贝同样适用:
import copy
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 创建一个包含自定义对象的列表
p1 = Person("张三", 20)
a = [p1, 100]
# 浅拷贝
c = copy.copy(a)
c[0].age = 30
print(p1.age) # 输出:30(浅拷贝,自定义对象仍然共享引用)
# 深拷贝
d = copy.deepcopy(a)
d[0].age = 40
print(p1.age) # 输出:30(深拷贝,自定义对象也被复制了)
六、实际应用场景
6.1 什么时候用浅拷贝
- 当你需要一个新的容器,但容器内部的元素不需要修改时
- 当容器只有一层,没有嵌套结构时
- 当你关心性能,且不需要完全独立的对象时
示例:
# 遍历列表并修改部分元素,不影响原列表
original = [1, 2, 3, 4, 5]
new_list = original.copy()
for i in range(len(new_list)):
if new_list[i] % 2 == 0:
new_list[i] *= 2
print(original) # 输出:[1, 2, 3, 4, 5]
print(new_list) # 输出:[1, 4, 3, 8, 5]
6.2 什么时候必须用深拷贝
- 当容器有多层嵌套结构时
- 当你需要完全独立的副本,修改任何部分都不影响原对象时
- 当容器内部包含可变对象(如列表、字典、自定义对象)时
示例:
# 保存游戏状态的快照
game_state = {
"player": {"name": "小明", "hp": 100, "position": [10, 20]},
"enemies": [{"name": "怪物1", "hp": 50}, {"name": "怪物2", "hp": 80}]
}
# 保存当前状态(必须用深拷贝)
saved_state = copy.deepcopy(game_state)
# 继续游戏,修改状态
game_state["player"]["hp"] = 50
game_state["player"]["position"][0] = 15
# 读取存档
game_state = saved_state
print(game_state["player"]["hp"]) # 输出:100
print(game_state["player"]["position"]) # 输出:[10, 20]
七、扩展知识点:其他拷贝相关技巧
7.1 字典的拷贝
字典也有自己的 copy() 方法,同样是浅拷贝:
original_dict = {"a": 1, "b": [2, 3]}
shallow_copy = original_dict.copy()
shallow_copy["b"][0] = 99
print(original_dict["b"]) # 输出:[99, 3](浅拷贝,嵌套列表共享引用)
# 深拷贝字典
deep_copy = copy.deepcopy(original_dict)
deep_copy["b"][1] = 100
print(original_dict["b"]) # 输出:[99, 3]
7.2 列表推导式与拷贝
列表推导式也可以实现浅拷贝:
original = [1, 2, 3, [4, 5]]
shallow_copy = [x for x in original]
shallow_copy[3][0] = 99
print(original[3]) # 输出:[99, 5]
7.3 性能对比
深拷贝比浅拷贝慢得多,尤其是对于大的嵌套结构:
import timeit
import copy
# 创建一个大的嵌套列表
big_list = [[i for i in range(100)] for j in range(100)]
# 测试浅拷贝性能
shallow_time = timeit.timeit(lambda: big_list.copy(), number=1000)
# 测试深拷贝性能
deep_time = timeit.timeit(lambda: copy.deepcopy(big_list), number=1000)
print(f"浅拷贝耗时: {shallow_time:.4f}秒")
print(f"深拷贝耗时: {deep_time:.4f}秒")
print(f"深拷贝比浅拷贝慢 {deep_time/shallow_time:.1f} 倍")
典型输出:
浅拷贝耗时: 0.0021秒
深拷贝耗时: 0.3456秒
深拷贝比浅拷贝慢 164.6 倍
八、总结
- 赋值操作:只是创建一个新的引用,指向同一个对象,修改任何一个都会影响另一个
- 浅拷贝:创建一个新的容器对象,但内部元素仍然共享引用,只适用于无嵌套或单层嵌套的情况
- 深拷贝:递归复制所有层级的对象,创建完全独立的副本,适用于多层嵌套的情况
- 性能考虑:浅拷贝速度快、内存占用少;深拷贝速度慢、内存占用多
- 不可变类型:不需要拷贝,Python会直接返回原对象的引用
最佳实践:
- 优先使用浅拷贝,只有在必须完全独立时才使用深拷贝
- 对于简单的单层结构,使用列表的
copy()方法或切片操作[:] - 对于多层嵌套结构,使用
copy.deepcopy() - 注意循环引用和自定义对象的拷贝行为
理解了深拷贝与浅拷贝的本质,你就再也不会遇到"修改一个列表,另一个列表也跟着变"的困惑了。在实际开发中,根据具体需求选择合适的拷贝方式,既能保证代码的正确性,又能提高程序的性能。
更多推荐


所有评论(0)