浅拷贝与深拷贝(Python 面试高频笔记)
面试回答
常见问法
- 浅拷贝和深拷贝有什么区别?
- 为什么这个问题经常和“可变对象”“嵌套结构”一起考?
=、list[:]、list()、copy.copy()、copy.deepcopy()分别是什么语义?- 为什么有时候明明“复制”了,改一个对象另一个还是会变?
- 深拷贝是不是一定更安全?工程里是不是都该用深拷贝?
回答
在 Python 里,变量名不是盒子,而是对象的引用(名字绑定)。所以讨论“拷贝”之前,要先区分:
=不是拷贝,只是让两个名字指向同一个对象- 浅拷贝:只复制最外层容器,内部子对象仍然共享引用
- 深拷贝:递归复制整个对象图,尽量让新旧对象彼此独立
所以这个问题经常和可变对象一起出现,因为只有当内部对象是可变的,比如 list、dict、set、自定义可变对象时,共享引用才容易带来副作用;如果内部元素都是 int、str、tuple 这类不可变对象,就算共享引用,通常也不会表现出“联动修改”。
一个标准回答可以这么说:
浅拷贝复制的是“外壳”,深拷贝复制的是“整棵对象引用结构”。 Python 里很多“复制后互相影响”的问题,本质不是拷贝失败,而是内部可变对象仍然共享。 所以要看需求:如果只想复制一层容器,浅拷贝就够;如果要彻底隔离嵌套状态,才考虑深拷贝。但深拷贝更贵,也不一定适合所有对象。
import copy
data = [[1, 2], [3, 4]]
x = data # 赋值:不是拷贝
a = copy.copy(data) # 浅拷贝
b = copy.deepcopy(data) # 深拷贝
data[0].append(99)
print(data) # [[1, 2, 99], [3, 4]]
print(x) # [[1, 2, 99], [3, 4]] 同一个对象
print(a) # [[1, 2, 99], [3, 4]] 外层新对象,但内层共享
print(b) # [[1, 2], [3, 4]] 完全独立
追问
-
为什么浅拷贝会“失效”? 不是失效,而是它本来就只复制外层,内部对象还共用。
-
为什么这个问题和可变对象强相关? 因为不可变对象即使共享,也改不了原对象本身;可变对象共享后,一方原地修改,另一方就会看到变化。
-
list[:]、list()、list.copy()、copy.copy()都是什么? 对列表来说本质上都是浅拷贝,都会创建新的外层列表,但元素引用通常仍共享。 -
深拷贝是不是递归复制一切? 原则上会递归复制对象图,但不是“无脑复制所有东西”。某些对象(如文件句柄、socket、模块、线程锁等)不适合或不能被正常深拷贝。
-
深拷贝为什么更贵? 因为它要遍历和复制整个对象图,还要处理循环引用,并维护
memo防止无限递归和重复复制。 -
自定义对象能控制深拷贝吗? 可以,能通过
__copy__和__deepcopy__自定义行为。
原理展开
1. 先把“赋值”和“拷贝”分开
很多人一上来就讲浅拷贝、深拷贝,但真正的第一层知识点是:
a = [1, 2, 3]
b = a
print(a is b) # True
这里 b = a 不是复制列表,只是让 b 也绑定到同一个列表对象。
所以面试里最好先说一句:
Python 变量保存的不是对象本身,而是对象的引用;赋值只是引用复制,不是对象复制。
这是面试官很爱听的一句,因为它体现你理解的是 Python 对象模型,不是只背 API。
2. 浅拷贝到底复制了什么
浅拷贝复制的是当前这一层容器对象本身,比如新建一个新的 list 或 dict,但容器里的元素如果本身还是对象,那只是把这些元素的引用复制过去。
lst1 = [[1], [2]]
lst2 = lst1.copy()
print(lst1 is lst2) # False,外层不是同一个对象
print(lst1[0] is lst2[0]) # True,内层还是同一个对象
所以浅拷贝的关键词是:
- 外层新
- 内层共享
这句话非常适合面试速记。
3. 深拷贝到底复制了什么
深拷贝会沿着对象引用继续向下复制,直到把整个嵌套结构都尽量做成独立副本。
import copy
lst1 = [[1], [2]]
lst2 = copy.deepcopy(lst1)
print(lst1 is lst2) # False
print(lst1[0] is lst2[0]) # False
所以深拷贝的关键词是:
- 递归复制
- 尽量隔离所有可变子对象
- 代价更高
注意这里说“尽量”,是因为并不是所有对象都适合深拷贝,也不是所有对象都能按预期深拷贝。
4. 为什么“可变性”是这道题的核心
因为拷贝问题的本质是:共享引用之后,会不会被原地修改影响。
不可变对象:共享通常没关系
a = [1, 2, 3]
b = a.copy()
a[0] = 100
print(b) # [1, 2, 3]
这里看起来没受影响,不是因为元素被深拷贝了,而是因为 int 不可变。
你修改 a[0] = 100 的本质是让 a 的第 0 个位置重新绑定到一个新的整数对象,而不是原地改那个整数。
可变对象:共享就容易出事
a = [[1], [2]]
b = a.copy()
a[0].append(99)
print(b) # [[1, 99], [2]]
这里 append 是原地修改内层列表,所以共享引用的另一方也看到了变化。
面试里很加分的一句话:
判断是否会互相影响,关键不是“有没有复制”,而是共享的那部分对象是否可变、是否发生原地修改。
5. 常见浅拷贝方式
下面这些在大多数基础题里都属于浅拷贝:
import copy
lst = [[1], [2]]
a = lst.copy()
b = lst[:]
c = list(lst)
d = copy.copy(lst)
print(a is lst, b is lst, c is lst, d is lst) # 都是 False
print(a[0] is lst[0]) # True
补充一个容易答错的点
列表推导式不一定是深拷贝。
lst = [[1], [2]]
x = [item for item in lst]
print(x is lst) # False
print(x[0] is lst[0]) # True
它只是重新构造了外层列表,元素本身仍然是原来的引用,所以依然是浅层效果。 除非你在推导式里显式复制元素,比如:
x = [item.copy() for item in lst]
但这也只是“手动多复制一层”,不是通用意义上的完整深拷贝。
6. dict、set、自定义对象也是同样逻辑
这个问题不要只会答 list,面试官很可能追问字典。
d1 = {"a": [1, 2], "b": [3, 4]}
d2 = d1.copy()
d1["a"].append(99)
print(d2) # {'a': [1, 2, 99], 'b': [3, 4]}
说明 dict.copy() 也是浅拷贝:
- 字典本身是新的
- value 如果是可变对象,仍然共享
自定义对象也一样,本质上复制的是对象及其属性引用结构,而不是“类实例天然就深拷贝”。
7. 深拷贝为什么更贵
深拷贝贵在三点:
① 要遍历整个对象图
嵌套层级越深、对象越多,成本越高。
② 要处理循环引用
import copy
a = []
a.append(a)
b = copy.deepcopy(a)
print(b is b[0]) # True
如果没有专门机制,递归会无限循环。
deepcopy 内部会用一个 memo 字典记录“已经复制过哪些对象”,既避免死循环,也避免重复复制同一个共享子对象。
③ 可能复制了很多本不需要复制的数据
比如大对象缓存、只读配置、共享模型实例等,盲目深拷贝会浪费内存和时间。
所以工程实践里,深拷贝不是默认更好,而是更重的隔离手段。
8. 自定义深拷贝行为
如果类里有些字段需要深拷贝,有些字段应该共享,就可以自定义。
import copy
class Config:
def __init__(self, options, cache):
self.options = options # 希望独立
self.cache = cache # 希望共享(大缓存)
def __deepcopy__(self, memo):
if id(self) in memo:
return memo[id(self)]
new_obj = self.__class__(
copy.deepcopy(self.options, memo),
self.cache
)
memo[id(self)] = new_obj
return new_obj
cfg1 = Config({"db": {"host": "localhost"}}, cache={"pool": "shared"})
cfg2 = copy.deepcopy(cfg1)
cfg1.options["db"]["host"] = "127.0.0.1"
print(cfg1.options) # {'db': {'host': '127.0.0.1'}}
print(cfg2.options) # {'db': {'host': 'localhost'}}
print(cfg1.cache is cfg2.cache) # True
这类回答非常有“工程味”: 你不仅知道 API,还知道什么时候不该深拷贝全部内容。
9. 工程实践怎么选
适合浅拷贝的场景
- 只需要复制一层容器
- 内部元素是不可变对象
- 明确允许共享内部对象
- 性能敏感,且结构简单
适合深拷贝的场景
- 多层嵌套配置、JSON、树结构
- 需要把模板对象改造成独立工作副本
- 要避免联动修改导致脏数据
- 单元测试中需要隔离输入状态
更推荐的工程思路
很多时候,与其盲目深拷贝,不如显式构造新对象。 原因是显式构造更可控、更清楚,也更容易保证性能。
例如:
new_user = {
"name": old_user["name"],
"tags": old_user["tags"][:], # 只复制确实需要隔离的字段
}
这比一把 deepcopy(old_user) 更能体现你真正理解数据结构。
对比总结
| 概念 | 是否创建新外层对象 | 是否递归复制内部对象 | 内部可变对象是否共享 | 典型写法 | 适用场景 | 易混点 |
|---|---|---|---|---|---|---|
| 赋值 | 否 | 否 | 是 | b = a | 只是起别名、传递引用 | 很多人误以为这是“复制” |
| 浅拷贝 | 是 | 否 | 是 | a.copy() / a[:] / list(a) / dict.copy() / copy.copy(a) | 只复制一层容器,性能优先 | “新对象”不等于“完全独立” |
| 深拷贝 | 是 | 是 | 否(通常) | copy.deepcopy(a) | 多层嵌套且必须隔离状态 | 更安全不等于更应该默认使用 |
常见方法对比
| 写法 | 针对对象 | 本质 | 备注 |
|---|---|---|---|
b = a | 所有对象 | 赋值 | 不是拷贝 |
lst[:] | list、部分序列 | 浅拷贝 | 只复制这一层 |
list(lst) | 可迭代对象 | 浅拷贝式重建外层列表 | 元素通常仍共享 |
lst.copy() | list | 浅拷贝 | 语义最直接 |
dict.copy() | dict | 浅拷贝 | value 若可变仍共享 |
copy.copy(x) | 通用对象 | 浅拷贝 | 会尝试调用对象的复制协议 |
copy.deepcopy(x) | 通用对象 | 深拷贝 | 递归复制,处理循环引用 |
相近概念区分
| 概念 | 本质 | 和拷贝的关系 | 面试关键句 |
|---|---|---|---|
| 名字绑定 | 变量名指向对象 | 是一切拷贝问题的前提 | Python 变量保存的是引用,不是值盒子 |
| 可变性 | 对象能否原地修改 | 决定共享后是否会相互影响 | 共享不可怕,可变且原地改才可怕 |
| 作用域 | 名字在哪能被访问 | 不直接决定拷贝行为 | 作用域管“名字可见性”,拷贝管“对象共享关系” |
| 拷贝语义 | 新旧对象如何共享内部状态 | 面试核心 | 浅拷贝复制外层,深拷贝复制对象图 |
易错点
-
把
=当成拷贝。 实际上只是两个名字绑定同一个对象。 -
认为“生成了新列表/新字典”就一定完全独立。 新的只是外层容器,不代表里面的对象也新建了。
-
把
list[:]、list()、列表推导式误认为深拷贝。 它们通常都只是浅层效果。 -
只会背“浅拷贝复制一层,深拷贝递归复制”,但解释不出为什么。 面试真正想听的是:Python 名字绑定 + 可变对象 + 引用共享。
-
认为不可变对象就“不存在拷贝问题”。 严格来说仍然存在共享,只是通常不会产生副作用。
-
认为深拷贝一定更安全,所以应该默认使用。 现实中深拷贝可能很慢、很占内存,而且有些对象不适合复制。
-
忽略循环引用。 这是区分“知道 API”和“理解实现机制”的分水岭。
-
自定义类里一把梭
deepcopy。 某些字段应该共享,比如连接池、缓存、日志对象、单例资源。 -
看到 tuple 就认为一定不涉及共享问题。
tuple本身不可变,但里面可以装可变对象,这仍然可能引发共享副作用。
t1 = ([1, 2], [3, 4])
t2 = copy.copy(t1)
t1[0].append(99)
print(t2) # ([1, 2, 99], [3, 4])
记忆技巧
-
一句话记忆: 浅拷贝 = 复制壳,内容共享 深拷贝 = 连内容也递归复制
-
判断口诀: 先看是不是
=,再看是不是嵌套,最后看内部对象可不可变。 -
排错口诀: “改一个,另一个也变了” 不要先怀疑 Python,先检查:
- 是不是赋值不是拷贝
- 是不是浅拷贝
- 共享的内部对象是不是可变对象
- 是否发生了原地修改(如
append、update、pop)
-
面试官喜欢的关键词: 名字绑定、引用共享、外层容器、内部可变对象、原地修改、对象图、循环引用、
memo
面试速答版
浅拷贝和深拷贝的区别在于是否递归复制内部对象。
在 Python 里变量保存的是对象引用,所以 = 不是拷贝,只是多个名字指向同一个对象。浅拷贝会创建新的外层容器,但内部元素如果还是可变对象,引用仍然共享;深拷贝会递归复制整个对象结构,让新旧对象尽量彼此独立。
这个问题经常和可变对象一起考,因为真正会产生副作用的是“共享了可变对象并发生原地修改”。工程里不要默认用深拷贝,因为它更贵,还要处理循环引用;只有在多层嵌套且必须隔离状态时才用。
面试加分版
这个问题我一般会先从 Python 的对象模型说起。
在 Python 里,变量名本质上是对对象的绑定,= 只是引用复制,不会产生新对象。真正的“拷贝”要分浅拷贝和深拷贝。
浅拷贝只复制最外层容器,比如新建一个列表或者字典,但容器内部的元素如果本身还是对象,那只是把这些对象的引用复制过去。所以浅拷贝的特点是外层新、内层共享。如果内部共享的是可变对象,比如嵌套列表、字典、自定义可变实例,那么一方原地修改,另一方也会看到变化。
深拷贝则会递归复制整个对象图,尽量把内部子对象也变成独立副本,所以更适合多层嵌套结构、配置模板复制、复杂 JSON 复制这类场景。但它不是默认最优方案,因为它开销更大,还要处理循环引用,内部通过 memo 机制避免无限递归和重复复制。并且在工程里,有些对象其实不应该被深拷贝,比如缓存、连接池、文件句柄之类的资源对象。
所以我的判断标准通常是:
如果只是复制一层容器,或者内部对象不可变,用浅拷贝就够;如果是多层嵌套并且明确要求状态隔离,再考虑深拷贝。再进一步,如果我只需要隔离部分字段,通常会优先选择显式构造新对象,而不是无脑 deepcopy,这样性能和语义都更可控。