🐍Python 语言基础

浅拷贝与深拷贝(Python 面试高频笔记)

面试回答

常见问法

  • 浅拷贝和深拷贝有什么区别?
  • 为什么这个问题经常和“可变对象”“嵌套结构”一起考?
  • =list[:]list()copy.copy()copy.deepcopy() 分别是什么语义?
  • 为什么有时候明明“复制”了,改一个对象另一个还是会变?
  • 深拷贝是不是一定更安全?工程里是不是都该用深拷贝?

回答

在 Python 里,变量名不是盒子,而是对象的引用(名字绑定)。所以讨论“拷贝”之前,要先区分:

  • = 不是拷贝,只是让两个名字指向同一个对象
  • 浅拷贝:只复制最外层容器,内部子对象仍然共享引用
  • 深拷贝:递归复制整个对象图,尽量让新旧对象彼此独立

所以这个问题经常和可变对象一起出现,因为只有当内部对象是可变的,比如 listdictset、自定义可变对象时,共享引用才容易带来副作用;如果内部元素都是 intstrtuple 这类不可变对象,就算共享引用,通常也不会表现出“联动修改”。

一个标准回答可以这么说:

浅拷贝复制的是“外壳”,深拷贝复制的是“整棵对象引用结构”。 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. 浅拷贝到底复制了什么

浅拷贝复制的是当前这一层容器对象本身,比如新建一个新的 listdict,但容器里的元素如果本身还是对象,那只是把这些元素的引用复制过去。

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. dictset、自定义对象也是同样逻辑

这个问题不要只会答 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,先检查:

    1. 是不是赋值不是拷贝
    2. 是不是浅拷贝
    3. 共享的内部对象是不是可变对象
    4. 是否发生了原地修改(如 appendupdatepop
  • 面试官喜欢的关键词: 名字绑定、引用共享、外层容器、内部可变对象、原地修改、对象图、循环引用、memo


面试速答版

浅拷贝和深拷贝的区别在于是否递归复制内部对象。 在 Python 里变量保存的是对象引用,所以 = 不是拷贝,只是多个名字指向同一个对象。浅拷贝会创建新的外层容器,但内部元素如果还是可变对象,引用仍然共享;深拷贝会递归复制整个对象结构,让新旧对象尽量彼此独立。 这个问题经常和可变对象一起考,因为真正会产生副作用的是“共享了可变对象并发生原地修改”。工程里不要默认用深拷贝,因为它更贵,还要处理循环引用;只有在多层嵌套且必须隔离状态时才用。


面试加分版

这个问题我一般会先从 Python 的对象模型说起。 在 Python 里,变量名本质上是对对象的绑定,= 只是引用复制,不会产生新对象。真正的“拷贝”要分浅拷贝和深拷贝。

浅拷贝只复制最外层容器,比如新建一个列表或者字典,但容器内部的元素如果本身还是对象,那只是把这些对象的引用复制过去。所以浅拷贝的特点是外层新、内层共享。如果内部共享的是可变对象,比如嵌套列表、字典、自定义可变实例,那么一方原地修改,另一方也会看到变化。

深拷贝则会递归复制整个对象图,尽量把内部子对象也变成独立副本,所以更适合多层嵌套结构、配置模板复制、复杂 JSON 复制这类场景。但它不是默认最优方案,因为它开销更大,还要处理循环引用,内部通过 memo 机制避免无限递归和重复复制。并且在工程里,有些对象其实不应该被深拷贝,比如缓存、连接池、文件句柄之类的资源对象。

所以我的判断标准通常是: 如果只是复制一层容器,或者内部对象不可变,用浅拷贝就够;如果是多层嵌套并且明确要求状态隔离,再考虑深拷贝。再进一步,如果我只需要隔离部分字段,通常会优先选择显式构造新对象,而不是无脑 deepcopy,这样性能和语义都更可控。