Python 深拷贝和浅拷贝有什么区别,什么时候该用?
先把“赋值”和“拷贝”分开
很多 Python 拷贝问题,最容易错在第一步:把赋值当成了复制。
赋值只是多了一个名字,两个变量仍然指向同一个对象。拷贝才会创建一个新对象。至于是只复制外层,还是连里面的对象一起复制,这就是浅拷贝和深拷贝的区别。
pythonoriginal = [1, 2, 3] assigned = original assigned[0] = 99 print(original) # [99, 2, 3]
上面没有发生拷贝,assigned 和 original 指向同一个列表。
如果使用 copy.copy(),外层列表会变成两个对象:
pythonimport copy original = [1, 2, 3] copied = copy.copy(original) copied[0] = 99 print(original) # [1, 2, 3]
这段代码看起来已经解决问题了,但只适用于没有嵌套可变对象的情况。真正让人踩坑的,通常是列表里还有列表、字典里还有列表、对象属性里还有可变对象。
浅拷贝复制外层,里面的对象仍然共享
浅拷贝会创建一个新的“外壳”。外层容器是新的,但容器里的元素还是原来的引用。
pythonimport copy original = [1, 2, [3, 4]] shallow = copy.copy(original) print(original is shallow) # False,外层列表不同 print(original[2] is shallow[2]) # True,嵌套列表仍然是同一个
所以修改顶层元素没问题:
pythonshallow[0] = 99 print(original) # [1, 2, [3, 4]]
但修改嵌套对象,就会影响原数据:
pythonshallow[2][0] = 99 print(original) # [1, 2, [99, 4]]
这也是很多“明明复制了一份,原数据还是被改了”的来源。
常见浅拷贝写法有哪些
不同容器有自己的浅拷贝写法。它们写法不同,本质接近:只复制外层容器。
pythonimport copy items = [1, 2, [3, 4]] copy1 = copy.copy(items) copy2 = items.copy() copy3 = items[:] copy4 = list(items)
字典也类似:
pythonoriginal = {"debug": False, "endpoints": ["api1", "api2"]} cloned = original.copy() cloned["debug"] = True cloned["endpoints"].append("api3") print(original["debug"]) # False print(original["endpoints"]) # ['api1', 'api2', 'api3']
debug 是顶层值,改副本不会影响原字典;endpoints 是嵌套列表,浅拷贝后仍然共享。
深拷贝会递归复制嵌套对象
深拷贝使用 copy.deepcopy()。它会尽量把对象内部引用到的对象也复制一份。
pythonimport copy original = [1, 2, [3, 4]] deep = copy.deepcopy(original) deep[2][0] = 99 print(original) # [1, 2, [3, 4]]
再看一个更接近业务数据的例子:
pythonimport copy data = { "users": [ {"name": "Alice", "scores": [85, 90, 78]}, {"name": "Bob", "scores": [92, 88, 95]}, ] } processed = copy.deepcopy(data) for user in processed["users"]: user["average"] = sum(user["scores"]) / len(user["scores"]) print("average" in data["users"][0]) # False
如果这里只做浅拷贝,users 列表和里面的用户字典仍然可能被共享,后续处理就容易污染原始数据。
可变对象和不可变对象对拷贝结果影响很大
讨论深浅拷贝时,先看对象能不能被原地修改。
- 不可变对象:
int、float、str、tuple、frozenset等。 - 可变对象:
list、dict、set、大多数自定义对象等。
不可变对象通常没必要复制。对整数、字符串这类对象,copy.copy() 和 copy.deepcopy() 往往会返回原对象。
pythonimport copy a = 42 b = copy.copy(a) c = copy.deepcopy(a) print(a is b) # True print(a is c) # True
但元组要稍微小心。元组本身不可变,不代表它里面的元素都不可变。
pythonimport copy t = (1, 2, [3, 4]) shallow = copy.copy(t) deep = copy.deepcopy(t) print(t is shallow) # True,浅拷贝直接复用元组 print(t[2] is deep[2]) # False,深拷贝复制了里面的列表
如果一个不可变容器里藏着可变对象,深拷贝仍然有意义。
什么时候用浅拷贝,什么时候用深拷贝
可以按“后面会不会改嵌套对象”来判断。
| 场景 | 更合适的方式 | 原因 |
|---|---|---|
| 只改列表或字典的顶层元素 | 浅拷贝 | 外层独立就够了,成本低 |
| 要改嵌套列表、嵌套字典 | 深拷贝 | 避免共享内部对象 |
| 数据完全只读 | 不拷贝 | 拷贝只会增加开销 |
| 配置模板生成多份独立配置 | 深拷贝 | 每份配置可能修改嵌套项 |
| 缓存对外返回数据 | 通常深拷贝 | 防止调用方改坏缓存 |
| 大型数据结构且性能敏感 | 谨慎深拷贝 | 可能复制大量对象 |
浅拷贝不是“低级版深拷贝”。它只是适合不同场景。比如复制一个简单列表后只追加顶层元素,用浅拷贝就很好:
pythonitems = [1, 2, 3] new_items = items.copy() new_items.append(4) print(items) # [1, 2, 3]
配置、数据处理、缓存、撤销功能里的用法
配置模板
配置对象经常有嵌套字段,比如 endpoints、headers、feature flags。用默认配置派生新配置时,深拷贝更稳妥。
pythonimport copy default_config = { "debug": False, "max_retries": 3, "timeout": 30, "endpoints": ["api1.example.com", "api2.example.com"], } config = copy.deepcopy(default_config) config["debug"] = True config["endpoints"].append("api3.example.com") print(default_config["debug"]) # False print(default_config["endpoints"]) # ['api1.example.com', 'api2.example.com']
数据处理
做清洗、补字段、格式转换时,是否拷贝取决于函数约定。如果函数承诺“不修改输入”,要么在内部复制,要么返回新数据结构。
pythonimport copy def add_average(data): result = copy.deepcopy(data) for user in result["users"]: user["average"] = sum(user["scores"]) / len(user["scores"]) return result
更推荐把这个约定写清楚:函数会不会修改参数,比它内部用了什么拷贝方式更重要。
缓存数据
缓存最怕外部代码拿到引用后直接修改。
pythonimport copy class DataCache: def __init__(self): self._cache = {} def set(self, key, value): self._cache[key] = copy.deepcopy(value) def get(self, key): if key not in self._cache: return None return copy.deepcopy(self._cache[key]) cache = DataCache() cache.set("data", {"items": [1, 2, 3]}) cached = cache.get("data") cached["items"].append(4) print(cache.get("data")) # {'items': [1, 2, 3]}
这里 set 和 get 都做深拷贝,是为了让缓存内部状态和调用方彻底隔开。代价是性能会下降,数据很大时要评估是否值得。
撤销和重做
撤销功能保存的是“当时的状态”。如果状态里有嵌套对象,通常要保存深拷贝快照。
pythonimport copy class EditorState: def __init__(self): self.document = {"blocks": []} self.history = [] def save(self): self.history.append(copy.deepcopy(self.document)) def add_block(self, text): self.save() self.document["blocks"].append({"text": text}) def undo(self): if self.history: self.document = self.history.pop()
如果只保存浅拷贝,历史记录里的嵌套 block 可能继续被当前文档共享,撤销出来的状态就不可靠。
自定义对象如何控制拷贝行为
对自定义类,Python 会尝试按对象属性进行拷贝。但有些对象需要自己定义规则:比如连接池、文件句柄、缓存、父子节点引用等。
可以实现 __copy__ 和 __deepcopy__。
pythonimport copy class Settings: def __init__(self, name, options): self.name = name self.options = options def __copy__(self): cls = type(self) new_obj = cls(self.name, self.options) return new_obj def __deepcopy__(self, memo): cls = type(self) new_obj = cls( copy.deepcopy(self.name, memo), copy.deepcopy(self.options, memo), ) memo[id(self)] = new_obj return new_obj original = Settings("prod", {"endpoints": ["api1"]}) shallow = copy.copy(original) deep = copy.deepcopy(original) shallow.options["endpoints"].append("api2") print(original.options) # {'endpoints': ['api1', 'api2']} deep.options["endpoints"].append("api3") print(original.options) # {'endpoints': ['api1', 'api2']}
__deepcopy__(self, memo) 里的 memo 很关键。它用来记录“某个对象已经复制过了”,既能保持共享关系,也能处理循环引用。
循环引用为什么不会轻易拖垮 deepcopy
循环引用就是对象之间互相指向。比如链表节点指回前一个节点,树节点存 parent,图结构里节点互相关联。
pythonimport copy a = [1, 2] b = [3, 4] a.append(b) b.append(a) cloned = copy.deepcopy(a) print(cloned[2][2] is cloned) # True
deepcopy 能处理这个例子,是因为它内部使用了 memo 字典。复制对象前先查 memo,如果这个对象已经复制过,就直接复用那份副本,而不是无限递归下去。
自定义 __deepcopy__ 时也要配合 memo:
pythonimport copy class Node: def __init__(self, value): self.value = value self.next = None def __deepcopy__(self, memo): if id(self) in memo: return memo[id(self)] new_node = type(self)(self.value) memo[id(self)] = new_node new_node.next = copy.deepcopy(self.next, memo) return new_node node1 = Node(1) node2 = Node(2) node1.next = node2 node2.next = node1 copied = copy.deepcopy(node1) print(copied.next.next is copied) # True
如果忘了写入 memo,遇到循环结构就可能递归到报错。
性能上别把 deepcopy 当默认选项
深拷贝要遍历对象图,数据越大、嵌套越深、对象越复杂,成本越高。它不只是“慢一点”,有时会复制出大量本来不需要复制的对象。
可以用一个简单脚本感受差异:
pythonimport copy import time large_data = { "items": [{"id": i, "tags": ["python", "copy"]} for i in range(10000)] } start = time.perf_counter() shallow = copy.copy(large_data) print(f"shallow: {time.perf_counter() - start:.6f}s") start = time.perf_counter() deep = copy.deepcopy(large_data) print(f"deep: {time.perf_counter() - start:.6f}s")
实际项目里,更常见的优化不是“让 deepcopy 更快”,而是减少不必要的复制:
- 只读数据直接传引用。
- 只改顶层结构时用浅拷贝。
- 只需要修改某个分支时,手动复制那条路径。
- 大对象考虑不可变数据结构,或者明确函数的修改边界。
- 缓存场景权衡安全和性能,不要对超大结果无脑深拷贝。
例如只改一个用户的分数,不一定要复制整份数据:
pythondata = { "users": { "alice": {"scores": [85, 90]}, "bob": {"scores": [92, 88]}, } } new_data = data.copy() new_data["users"] = data["users"].copy() new_data["users"]["alice"] = data["users"]["alice"].copy() new_data["users"]["alice"]["scores"] = data["users"]["alice"]["scores"].copy() new_data["users"]["alice"]["scores"].append(100) print(data["users"]["alice"]["scores"]) # [85, 90]
这段写法比 deepcopy(data) 啰嗦,但在大数据结构里可能更可控。
容易忽略的边界
有些对象不适合拷贝
模块、函数、文件对象、网络连接、数据库连接这类对象,通常不应该被深拷贝。对它们来说,“复制一份”本身就不一定有合理语义。
pythonimport copy f = open("example.txt", "w") try: copy.deepcopy(f) except TypeError as e: print(type(e).__name__) finally: f.close()
如果类里持有这类资源,最好在 __copy__ 或 __deepcopy__ 里明确处理:共享、重新创建,或者直接禁止复制。
深拷贝会保持共享关系
如果原对象里两个字段指向同一个列表,deepcopy 后它们仍然会指向同一个“副本列表”,而不是变成两个独立列表。
pythonimport copy shared = [1, 2] original = {"a": shared, "b": shared} cloned = copy.deepcopy(original) print(cloned["a"] is cloned["b"]) # True print(cloned["a"] is shared) # False
这也是 memo 的作用之一:不是机械地“见一次复制一次”,而是在副本里保留原来的引用关系。
一套实用判断方式
写代码时可以先问三个问题:
- 后续会不会修改这个对象?不会,就别拷贝。
- 只改外层,还是会改里面的列表、字典、对象属性?只改外层,用浅拷贝;会改里面,用深拷贝或手动复制相关分支。
- 数据大不大、对象里有没有资源句柄或循环引用?如果有,别直接把
deepcopy当万能按钮。
一句话概括:赋值不会复制对象;浅拷贝只换外壳;深拷贝会递归复制内部对象,但要付出性能和语义成本。配置模板、数据清洗、缓存隔离、撤销快照这些场景,深拷贝很有用;简单列表、只读数据和性能敏感路径,浅拷贝甚至不拷贝往往更合适。