Python 浅拷贝与深拷贝:为什么我改了 b,a 也跟着变了?
Python 浅拷贝与深拷贝:为什么我改了 b,a 也跟着变了?
在 Python 中,列表、字典、集合这类对象都属于可变对象。
也正因为它们“可变”,所以在复制数据时,经常会遇到一个非常经典的问题:
明明我改的是
b,为什么a也跟着变了?
这背后涉及三个概念:
- 直接赋值;
- 浅拷贝;
- 深拷贝。
本文会从一个简单列表开始,一步步讲清楚它们之间的区别。
1. 直接赋值不是复制
先看一段最普通的代码:
a = [1, 2, 3]
b = a
print(b)
输出结果:
[1, 2, 3]
看起来 b 好像得到了一个和 a 一模一样的列表。
于是我们尝试修改 b:
b[0] = 0
print(a)
结果是:
[0, 2, 3]
问题来了:我们明明改的是 b,为什么 a 也变了?
原因是:
b = a并没有创建一个新列表,它只是让b也指向了a指向的那个列表对象。
也就是说,a 和 b 是两个变量名,但它们指向的是同一块内存里的同一个列表。
可以这样理解:
a ─┐
├──> [1, 2, 3]
b ─┘
所以无论通过 a 修改,还是通过 b 修改,操作的都是同一个列表。
2. 使用 copy 创建浅拷贝
如果我们真的想得到一个新的列表,可以使用列表的 copy() 方法:
a = [1, 2, 3]
b = a.copy()
print(b)
输出结果:
[1, 2, 3]
这时再修改 b:
b[0] = 0
print(a)
输出结果:
[1, 2, 3]
这一次,a 没有受到影响。
这是因为 a.copy() 创建了一个新的列表对象。
可以理解为:
a ──> [1, 2, 3]
b ──> [1, 2, 3]
两个列表的第一层结构已经分开了。
这就是浅拷贝。
3. 浅拷贝只复制第一层
浅拷贝看起来已经解决问题了,但它有一个非常重要的限制:
浅拷贝只复制最外层容器,不会递归复制内部嵌套的可变对象。
来看一个嵌套列表:
a = [1, 2, 3, [4, 5, 6, [7, 8, 9]]]
b = a.copy()
b[3][1] = 9
print(a)
输出结果:
[1, 2, 3, [4, 9, 6, [7, 8, 9]]]
这一次,a 又变了。
原因是 a.copy() 只复制了最外层列表。
最外层确实变成了两个不同的列表:
a ──> [1, 2, 3, 内层列表]
b ──> [1, 2, 3, 内层列表]
但是第四个元素里的内层列表,仍然是共享的。
更准确地说:
a[3] ─┐
├──> [4, 5, 6, [7, 8, 9]]
b[3] ─┘
所以当我们执行:
b[3][1] = 9
修改的是共享的内层列表。
因此 a 也会看到变化。
这就是浅拷贝最容易踩坑的地方。
4. 使用 deepcopy 创建深拷贝
如果我们希望嵌套结构里的每一层都被真正复制,就需要使用深拷贝。
Python 提供了内置模块 copy:
import copy
使用 copy.deepcopy() 可以创建深拷贝:
import copy
a = [1, 2, 3, [4, 5, 6, [7, 8, 9]]]
b = copy.deepcopy(a)
b[3][1] = 9
print(a)
输出结果:
[1, 2, 3, [4, 5, 6, [7, 8, 9]]]
这一次,a 没有变化。
原因是深拷贝会递归复制对象内部的嵌套对象。
可以理解为:
a ──> [1, 2, 3, [4, 5, 6, [7, 8, 9]]]
b ──> [1, 2, 3, [4, 5, 6, [7, 8, 9]]]
不仅最外层列表是新的,里面嵌套的列表也是新的。
所以修改 b 的内部元素,不会影响 a。
5. 直接赋值、浅拷贝、深拷贝对比
可以用一张表总结它们的区别:
| 操作 | 是否创建新对象 | 是否复制嵌套对象 | 适用场景 |
|---|---|---|---|
b = a |
否 | 否 | 只是想多一个变量名引用同一个对象 |
b = a.copy() |
是,只复制第一层 | 否 | 列表内部没有嵌套可变对象 |
b = copy.deepcopy(a) |
是 | 是,递归复制 | 数据结构有嵌套,并且希望完全独立 |
简单记忆:
- 赋值:同一个对象,两个名字;
- 浅拷贝:外层新对象,内层可能共享;
- 深拷贝:外层和内层都尽量创建新对象。
6. 为什么不可变对象看起来没这个问题
如果列表里放的是整数、字符串、元组这类不可变对象,浅拷贝通常不容易暴露问题。
例如:
a = [1, 2, 3]
b = a.copy()
b[0] = 0
这里修改 b[0],并不是把原来的整数 1 改成 0。
整数是不可变对象。
这一步真正发生的是:让 b[0] 指向另一个整数对象 0。
所以 a[0] 不会受到影响。
但如果列表里嵌套的是另一个列表、字典、集合这类可变对象,浅拷贝就可能出现共享内部对象的问题。
7. 什么时候用浅拷贝,什么时候用深拷贝
在实际开发中,可以按下面的思路选择:
如果数据结构很简单,比如:
[1, 2, 3]
["Python", "Java", "Go"]
使用浅拷贝通常就够了:
b = a.copy()
如果数据结构存在嵌套,并且你会修改嵌套对象,比如:
[
{"name": "张三", "scores": [90, 80]},
{"name": "李四", "scores": [85, 88]},
]
这时更适合使用深拷贝:
import copy
b = copy.deepcopy(a)
不过也要注意,深拷贝不是越多越好。
因为深拷贝会递归复制对象,数据结构越复杂,开销越大。
如果只是读取数据,不需要修改,就没有必要深拷贝。
8. 小结
本文通过列表示例讲清楚了 Python 中直接赋值、浅拷贝和深拷贝的区别。
核心结论如下:
b = a不是复制,而是让两个变量指向同一个对象;a.copy()是浅拷贝,只复制最外层容器;- 浅拷贝遇到嵌套可变对象时,内部对象仍然可能共享;
copy.deepcopy()是深拷贝,会递归复制内部对象;- 数据结构简单时用浅拷贝,嵌套结构需要独立修改时用深拷贝。
如果用一句话总结:
赋值复制的是引用,浅拷贝复制第一层,深拷贝复制整个嵌套结构。
更多推荐
所有评论(0)