Python 中深拷贝和浅拷贝有什么区别?
这类 Bug 看日志很烦。
接口明明只改了一个订单的扩展字段,结果缓存里的原始订单也跟着变了。更恶心的是,打印 id() 看第一层对象还真不一样。
像这样:
import copy
raw_order = {
"order_no": "A20260510001",
"items": [
{"sku": "P1001", "count": 2},
{"sku": "P2008", "count": 1},
],
"tags": ["new_user"]
}
draft_order = copy.copy(raw_order)
draft_order["order_no"] = "A20260510001_TMP"
draft_order["items"][0]["count"] = 99
draft_order["tags"].append("manual_check")
print(raw_order)
输出大概是这样:
{
'order_no': 'A20260510001',
'items': [{'sku': 'P1001', 'count': 99}, {'sku': 'P2008', 'count': 1}],
'tags': ['new_user', 'manual_check']
}
第一眼我就不太信这个 copy.copy()。
order_no 没变,说明最外层字典确实被复制了一份。
但 items 和 tags 变了,说明里面那几个列表、字典根本没复制,只是引用还指向原来的对象。
这就是浅拷贝。
浅拷贝只复制最外面一层容器。里面装的对象,不管是列表、字典,还是自定义对象,基本都还是原来的引用。
可以直接把地址打出来看:
import copy
raw_order = {
"items": [{"sku": "P1001", "count": 2}],
"tags": ["new_user"]
}
draft_order = copy.copy(raw_order)
print(id(raw_order), id(draft_order))
print(id(raw_order["items"]), id(draft_order["items"]))
print(id(raw_order["items"][0]), id(draft_order["items"][0]))
第一行不一样,后面两行大概率一样。
这地方很多人容易误判,因为他们只看了最外层对象。
如果数据只有一层,浅拷贝没问题:
origin = {"name": "order_job", "retry": 3}
copied = origin.copy()
copied["retry"] = 5
print(origin) # {'name': 'order_job', 'retry': 3}
print(copied) # {'name': 'order_job', 'retry': 5}
这种场景用 dict.copy() 就够了,别上来就 deepcopy。我一般看到有人在很大的配置对象上无脑 deepcopy,会多看两眼。不是不能用,是它可能把你不想拷贝的东西也递归扫一遍,性能和行为都容易变得不受控。
深拷贝就是另一套玩法。
它不只复制第一层,还会继续往里面走,把嵌套对象也复制出来:
import copy
raw_order = {
"order_no": "A20260510001",
"items": [
{"sku": "P1001", "count": 2},
{"sku": "P2008", "count": 1},
],
"tags": ["new_user"]
}
draft_order = copy.deepcopy(raw_order)
draft_order["items"][0]["count"] = 99
draft_order["tags"].append("manual_check")
print(raw_order)
print(draft_order)
这次 raw_order 不会被带着改。
这才是很多业务里想要的“复制一份出来随便改”。
比如导入订单时,先拿原始数据做一份临时草稿,清洗字段、补默认值、打标记,最后再决定要不要入库。这个时候用浅拷贝就很危险,因为你以为自己在改草稿,实际把原始数据污染了。
我现场一般会这么写,不会把清洗逻辑直接怼在原对象上:
import copy
defbuild_order_preview(order_payload: dict) -> dict:
preview = copy.deepcopy(order_payload)
for line in preview.get("items", []):
line["count"] = int(line.get("count") or0)
line["amount"] = round(line["count"] * float(line.get("price") or0), 2)
preview.setdefault("check_flags", [])
ifnot preview.get("receiver_mobile"):
preview["check_flags"].append("missing_mobile")
return preview
这里用 deepcopy 是有理由的。
因为 items 是列表,列表里又是字典。后面还要改 count、补 amount。这要是浅拷贝,原始请求参数也会被改掉。后面你再打日志排查,会发现入参日志和用户传过来的数据对不上,能把人绕进去。
不过深拷贝也不是银弹。
有些对象不适合随便深拷贝,比如文件句柄、数据库连接、线程锁、一些客户端对象。业务对象里如果混了这些东西,deepcopy 可能直接报错,也可能复制出一个看着能用但状态很怪的对象。
看个自定义类的例子:
import copy
from dataclasses import dataclass, field
@dataclass
classImportTask:
task_id: str
rows: list[dict]
errors: list[str] = field(default_factory=list)
task = ImportTask(
task_id="import_20260510",
rows=[{"line": 1, "mobile": "13800000000"}]
)
task_copy = copy.copy(task)
task_deep = copy.deepcopy(task)
task_copy.rows[0]["mobile"] = "invalid"
task_copy.errors.append("mobile_format_error")
print(task.rows)
print(task.errors)
这个 task_copy 看着是新对象,实际上 rows 和 errors 还连着原来的。
用在导入任务里就很坑。你本来想复制一个任务出来做预校验,结果错误信息挂回了原任务,后面状态流转就乱了。
如果对象里有些字段你明确不想深拷贝,可以自己控制:
from dataclasses import dataclass, field
import copy
@dataclass
classPriceCalcContext:
batch_no: str
rules: list[dict]
cache_client: object = None
logs: list[str] = field(default_factory=list)
defclone_for_retry(self):
return PriceCalcContext(
batch_no=self.batch_no,
rules=copy.deepcopy(self.rules),
cache_client=self.cache_client,
logs=[]
)
这里我只深拷贝 rules,因为重试时规则可能会临时改。
cache_client 不拷贝,继续复用。
logs 直接给新的空列表,避免上一次重试的日志混进来。
这个比无脑 deepcopy(self) 更稳。
平时判断用哪个,我一般就看一句话:后面会不会改嵌套对象。
只改第一层,用浅拷贝。
要改里面的列表、字典、对象,用深拷贝。
对象里混了连接、锁、客户端、上下文句柄,就别偷懒,自己写一个明确的复制方法。
Python 这块最容易坑人的地方,不是 copy.copy 和 copy.deepcopy 名字记不住,而是你以为复制了,其实只复制了壳。
壳换了,里面还是同一套东西。线上很多脏数据,就是这么悄悄写进去的。