我把一个嵌套的字典拷贝了一份、想改副本而不动原件,结果一改副本里层的数据、原件竟然也跟着变了,排查半天才发现我用的拷贝只复制了最外层、里面那层还是和原件共享的同一个对象的深度复盘
这是一次让我对"'复制一份'到底复制到了哪一层"有了刻骨认知的事故。我有个嵌套的数据结构(一个字典,里面的值又是列表/字典),需要在不影响原件的前提下,拷一份出来改改、做点试探性的处理。我很自然地用 copy()(或切片 [:]、或 dict(原件))拷了一份,心想:这下副本和原件就是两份独立的数据了,我随便改副本,原件纹丝不动。
可改着改着,诡异的事发生了:我明明只改了副本里嵌套那一层的数据(比如 副本["items"].append(x)),可一回头去看原件,原件里对应的那层数据也跟着变了!我盯着代码反复看:我拷贝了啊,改的是副本啊,原件怎么会动?我一度怀疑是哪里引用串了、是别处偷偷改了原件,查了半天毫无头绪。直到我去深究 Python 的 copy() 到底做了什么,才恍然大悟:我用的是浅拷贝(shallow copy)——它只复制了最外层这个容器(创建了一个新的字典/列表),可容器里面装的那些元素(嵌套的列表、字典),复制的只是它们的引用,而不是它们本身。也就是说,副本的最外层是新的,但副本里那层嵌套对象,和原件里的还是同一个对象!于是我通过副本去修改那个嵌套对象,原件因为指向的是同一个,自然也"变"了。
故障现场:浅拷贝只新建了外层,内层还是共享同一个对象
我把这个"改副本影响原件"的现象还原出来,问题一目了然:
import copy
original = {"name": "A", "items": [1, 2, 3]}
# 浅拷贝: copy()/dict()/[:] 都只复制最外层
shallow = original.copy() # 或 dict(original)
# 改副本【最外层】的值: 原件不受影响(外层是新的)
shallow["name"] = "B"
print(original["name"]) # "A" ✓ 原件没变(外层独立)
# 改副本里【嵌套对象】: 原件也跟着变了!(内层是共享的同一个)
shallow["items"].append(4)
print(original["items"]) # [1, 2, 3, 4] ✗ 原件也变了!
# ↑ 因为 shallow["items"] 和 original["items"] 是【同一个列表对象】
# 浅拷贝只复制了它的引用, 没复制列表本身
# 验证: 它俩内层是不是同一个对象
print(shallow["items"] is original["items"]) # True ← 同一个!
# 正解: 深拷贝, 递归复制所有层级, 真正独立
deep = copy.deepcopy(original)
deep["items"].append(99)
print(original["items"]) # [1, 2, 3, 4] ✓ 原件不受影响
print(deep["items"] is original["items"]) # False ← 不同对象
# 列表同理: [:]、list()、copy() 都是浅拷贝
lst = [[1, 2], [3, 4]]
sh = lst[:] # 浅拷贝
sh[0].append(99) # 改内层
print(lst[0]) # [1, 2, 99] ✗ 原件内层也变了
看着"改副本内层、原件也变、且 is 判断它俩内层是同一个对象",我才彻底明白:copy()、[:]、dict()、list() 这些常用的拷贝方式,都是浅拷贝——它们只新建了最外层的那个容器,而容器里装的元素,复制的只是引用(对于嵌套的可变对象,副本和原件里装的还是同一个对象)。所以"改最外层"不会互相影响(外层确实是两个独立容器),但"改嵌套对象内部"就会互相影响(那个嵌套对象是共享的)。我以为"拷一份"就是"从里到外彻底复制成完全独立的两份",可浅拷贝只独立了第一层。我以为我手里是两份互不相干的数据,其实它们只是"外壳不同、内核共享"。
第一件事:搞懂浅拷贝与深拷贝——复制到第几层是关键
冷静下来,我去把"Python 的浅拷贝与深拷贝"这一课认真补了,才明白这个"改副本动原件"的根源:
【浅拷贝 vs 深拷贝, 区别在"复制到第几层"】
Python 变量存的是"对象的引用"; 拷贝有"深浅"之分:
浅拷贝(shallow copy): 只复制【最外层容器】
- copy()、dict(d)、list(l)、l[:]、{**d}、[*l] 都是浅拷贝
- 新建了外层容器, 但里面的元素只复制【引用】
- → 嵌套的可变对象(内层 list/dict), 副本和原件【共享同一个】
- → 改外层不互相影响; 改嵌套对象内部会互相影响
深拷贝(deep copy): 递归复制【所有层级】
- copy.deepcopy(x)
- 从里到外每一层都创建全新对象, 副本和原件完全独立
- → 改副本任何一层都不影响原件
为什么默认是浅拷贝:
- 浅拷贝快、省内存; 深拷贝要递归复制整棵结构, 慢、占内存
- 很多场景只需独立外层(或数据本就不可变), 浅拷贝就够
- 但只要涉及"改嵌套的可变对象"且不想影响原件 → 必须深拷贝
如何选:
- 数据是扁平的、或元素都不可变(int/str/tuple)→ 浅拷贝足够
- 嵌套且会改内层、要真正独立 → copy.deepcopy
- 不可变数据结构 / 函数式风格(不改原件, 返回新结构)→ 从根上避免此坑
注意: 这也是"可变默认参数""列表乘法共享引用"等坑的同一内核——
多个名字指向【同一个可变对象】, 通过一个改了, 另一个也看到
这一下点醒了我:我把"拷贝一份"笼统地理解成了"得到一份从里到外完全独立的数据",可拷贝其实有"深浅"之分——浅拷贝只复制最外层容器、内层元素只复制引用,深拷贝才递归复制所有层级。我用的 copy() 是浅拷贝,所以副本和原件只在最外层独立、嵌套对象仍是共享的同一个;我去改那个共享的嵌套对象,两边自然都"变"了。这背后,和"可变默认参数""列表乘法共享引用"是同一个内核——多个名字指向同一个可变对象,通过一个改了,另一个也看到。不是拷贝失灵,是我没意识到拷贝有深浅、而我需要的是深的那种。
第二件事:正解——要真正独立用 deepcopy,或用不可变结构从根上避坑
找到根因,正解就清晰了:要"改副本完全不影响原件"且数据是嵌套的,用 copy.deepcopy(递归复制所有层级、真正独立);如果只需独立外层、或元素都不可变,浅拷贝就够;更彻底的做法是用不可变数据结构 / 函数式风格(不改原件、每次返回新结构),从根上避开"共享可变对象"这个坑。
import copy
original = {"name": "A", "items": [1, 2, 3], "meta": {"k": "v"}}
# 错误: 浅拷贝, 改嵌套对象会影响原件
bad = original.copy()
bad["items"].append(4) # ✗ original["items"] 也变了
# 正解1: 嵌套结构要真正独立 → deepcopy(递归复制所有层级)
good = copy.deepcopy(original)
good["items"].append(4) # ✓ 只改 good, original 不受影响
good["meta"]["k"] = "v2" # ✓ 内层也独立
# 正解2: 只需独立外层 / 元素不可变 → 浅拷贝够(更快更省)
flat = {"a": 1, "b": "x"} # 值都不可变
sh = flat.copy() # 浅拷贝即可, 改副本不影响原件(值不可变)
# 正解3: 不改原件, 返回新结构(函数式 / 不可变思路, 从根上避坑)
def with_item(d, item):
return {**d, "items": [*d["items"], item]} # 不动 d, 返回新字典+新列表
new = with_item(original, 4) # original 完全不变
# 正解4: 真正需要"不可变"语义的, 用 tuple/frozenset 等不可变类型
point = (1, 2) # tuple 不可变, 不存在被改的问题
# 判断要不要 deepcopy 的关键: "我会不会改嵌套的可变对象、且不想影响原件?"
# 会 → deepcopy; 不会(只改外层/元素不可变)→ 浅拷贝够
这套做法的精髓,是先想清楚"我需要独立到第几层",再选对应的拷贝方式:要从里到外完全独立(且会改嵌套对象),就用 deepcopy;只需外层独立或元素不可变,浅拷贝又快又省;而最省心的,是干脆不去改共享的可变对象——用函数式风格"不改原件、返回新结构",或用 tuple 等不可变类型,从根上消除"共享可变对象被意外修改"的可能。不是 deepcopy 万能(它慢、占内存),而是按"需要的独立深度"选对工具。
【拷贝数据, 几条原则】
1. copy()/dict()/list()/[:]/{**d}/[*l] 都是浅拷贝: 只独立外层, 内层共享
2. 嵌套结构 + 会改内层 + 不想影响原件 → 用 copy.deepcopy(递归独立)
3. 只需独立外层 / 元素都不可变 → 浅拷贝够(deepcopy 慢、占内存, 别滥用)
4. 函数式/不可变思路: 不改原件、返回新结构({**d,...}/[*l,...]), 从根上避坑
5. 真正不可变的数据用 tuple/frozenset, 不存在被意外修改的问题
6. 判断标准: 我会不会改嵌套的可变对象、且不想让原件受影响?
第三件事:其他"以为独立、其实共享同一个可变对象"的同类坑
顺着"多个名字指向同一个可变对象、改一个动另一个"这条线,我把同类的坑都梳理了一遍,它们都是同一个内核:
第一个,可变默认参数。def f(x=[]) 的默认列表在多次调用间是同一个对象,一次往里加了东西,下次调用还看得到。该用 None 占位、函数内新建。
第二个,列表乘法/推导共享引用。[[0]*3]*3 里三个内层列表是同一个对象,改一行全行变。该用列表推导各建一个。
第三个,直接赋值不是拷贝。b = a 只是给同一个对象起了第二个名字,改 b 就是改 a。要副本得显式拷贝。
第四个,把同一个对象放进多个容器/缓存。同一个可变对象被多处引用,一处改了所有引用都看到。要隔离就各存一份副本,或存不可变快照。
第四件事:赋值 vs 浅拷贝 vs 深拷贝,一张表对照
我把"直接赋值""浅拷贝""深拷贝"在独立性上的差别整理成一张表,这是我现在决定怎么拷数据的依据:
| 方式 | 外层独立吗 | 嵌套内层独立吗 | 改副本影响原件吗 |
|---|---|---|---|
| b = a(赋值) | ✗ 同一对象 | ✗ | 改任何层都影响(就是同一个) |
| a.copy()/[:]/dict(a)(浅) | ✓ 新外层 | ✗ 共享 | 改外层不影响, 改内层影响 |
| copy.deepcopy(a)(深) | ✓ | ✓ 递归独立 | 改任何层都不影响 |
| 不可变结构 tuple/frozenset | — | — | 根本不能改, 不存在此问题 |
这张表让我看清:赋值根本不是拷贝(同一个对象);浅拷贝只独立外层、内层共享;只有深拷贝才从里到外完全独立。我要的是"改副本内层不影响原件",而我用了只独立外层的浅拷贝,自然翻车。独立到第几层,取决于我选哪种拷贝。
第五件事:我对"拷贝一份"的几个想当然
这次事故,本质是我把"拷贝"想当然地等同于了"完全独立的两份"。把这些想当然列出来,每一条都值得警惕:
| 我曾经的想当然 | 事故教我的真相 |
|---|---|
| "copy() 拷一份就是两份完全独立的数据" | 它是浅拷贝, 只独立外层, 嵌套对象仍共享 |
| "改副本绝不会影响原件" | 改副本里共享的嵌套对象, 原件也变 |
| "改副本动了原件, 一定是别处串改了" | 常是浅拷贝导致内层共享同一对象 |
| "[:]/dict()/list() 是深拷贝" | 它们都是浅拷贝, 只复制外层 |
| "b = a 就是拷了一份给 b" | 赋值只是起别名, a/b 是同一个对象 |
| "反正都用 deepcopy 最保险" | deepcopy 慢占内存, 按需用; 浅拷贝够就别滥用 |
第六件事:拷贝数据、想要独立副本时,我现在的自检习惯
现在每当我拷贝数据、想要一份独立副本,或排查"改副本却动了原件",我都会先按这张图问自己:
这张图的精髓,是"先看数据嵌不嵌套、会不会改内层;改嵌套内层又要独立就用 deepcopy,别拿浅拷贝当深拷贝"。写时就嵌套要独立用 deepcopy、扁平用浅拷贝、或用不可变结构/函数式避坑、排查就看改副本动原件是不是浅拷贝让内层共享了同一对象(用 is 验证)。这套习惯,让我从"拷一份就是独立两份"变成了"拷贝有深浅、按需选独立深度"——核心始终是:Python 变量存的是对象引用,拷贝有深浅之分:copy()/dict()/list()/[:]/{**d} 都是浅拷贝、只新建最外层容器而里面的嵌套可变对象只复制引用(副本和原件共享同一个),所以改外层不互相影响、改嵌套对象内部会互相影响;copy.deepcopy 才递归复制所有层级、副本与原件完全独立;赋值 b=a 根本不是拷贝(同一对象);正解是按需要的独立深度选——嵌套且会改内层要真正独立用 deepcopy、只需独立外层或元素不可变用浅拷贝、或用函数式不改原件返回新结构/用不可变类型从根上避坑;这与可变默认参数、列表乘法共享引用是同一内核——多个名字指向同一可变对象。
我立下的几条规矩
这场"改副本动了原件"的事故,换来了我写 Python 时,刻进骨子里的几条铁律:
- copy()/dict()/list()/[:]/{**d}/[*l] 都是浅拷贝:只独立最外层,嵌套的可变对象副本和原件共享同一个。
- 浅拷贝下改外层不互相影响,但改嵌套对象内部会互相影响(那是共享的同一个对象)。
- 嵌套结构 + 会改内层 + 要真正独立 → 用 copy.deepcopy 递归复制所有层级。
- 只需独立外层、或元素都不可变 → 浅拷贝就够;deepcopy 慢占内存,别滥用。
- 赋值 b = a 不是拷贝,只是给同一对象起别名,改 b 就是改 a。
- 更省心:用函数式风格(不改原件、返回新结构)或不可变类型(tuple/frozenset)从根上避坑。
- 这与可变默认参数、列表乘法共享引用是同一内核:多个名字指向同一可变对象,改一个动另一个。
附:一段把"浅拷贝共享内层、深拷贝彻底独立"摆清楚的小实验
这是我后来写的一段小实验,把赋值、浅拷贝、深拷贝对嵌套结构的影响并排跑出来——它把"分离到第几层"这个抽象的事变成了眼见为实的对比,现在我也常拿它给同事讲清这个坑:
import copy
def show(label, src, dst):
"""改 dst 的内层, 看 src 受不受影响 + 内层是不是同一个对象"""
dst["items"].append(999) # 改副本的【内层】
print(f"{label:8} 改副本内层后, 原件 items = {src['items']}, "
f"内层同一对象? {dst['items'] is src['items']}")
base = {"name": "A", "items": [1, 2, 3]}
# 赋值: 同一个对象, 改谁都一样
a = base; show("赋值", base, a)
# 输出: 赋值 ...原件 items=[1,2,3,999], 内层同一对象? True
base = {"name": "A", "items": [1, 2, 3]}
# 浅拷贝: 外层独立, 内层共享
sh = base.copy(); show("浅拷贝", base, sh)
# 输出: 浅拷贝 ...原件 items=[1,2,3,999], 内层同一对象? True ← 内层还是共享!
base = {"name": "A", "items": [1, 2, 3]}
# 深拷贝: 彻底独立
dp = copy.deepcopy(base); show("深拷贝", base, dp)
# 输出: 深拷贝 ...原件 items=[1,2,3], 内层同一对象? False ← 真正独立
# 一眼看清:
# 赋值/浅拷贝 → 内层 is 判断为 True(同一个对象), 改副本内层会动原件
# 深拷贝 → 内层 is 判断为 False(不同对象), 改副本任何层都不动原件
这段实验把这次的教训摆得明明白白:赋值和浅拷贝那两行,内层的 is 判断都是 True——副本和原件的内层是同一个对象,所以改副本内层、原件也变;只有深拷贝那行 is 是 False、原件 items 纹丝不动,才是真正彻底独立的两份。跑完这段我才真正在脑子里刻下:判断两份数据是不是真独立,别看它俩外层是不是两个对象,要看我会去改的那一层、那个嵌套对象,is 判断是不是同一个;只要还是同一个,改一个就会动另一个。用 is 把"共享的内核"亲手验出来、看着它从 True 变成 False,比记任何"嵌套要用 deepcopy"的口诀都更让我对"分离的深度"心里有数。
写在最后
回头看,这场由"浅拷贝"引发的"改副本动原件"事故,真正教给我的,远不止"嵌套结构用 deepcopy"这一个技巧。它让我对"当我们说'复制一份'、'分开'、'独立'时, 这种'分离'常常只是'表层的分离'——表面上是两个东西了, 可它们的'内核'、'深处依赖的那部分', 可能依然是同一个、共享的; 而我们误以为已经彻底分开、可以各自折腾互不影响, 一旦动了那个共享的内核, 才发现牵一发而动全身",有了一次刻骨的体会。我栽跟头,是因为我把'表层的分离'当成了'彻底的、贯穿到底的分离'——我看到副本和原件是两个不同的字典(外层确实分开了), 就以为它俩从里到外都是独立的两份;我没意识到, 这个"分开"只发生在最外层; 它们内部嵌套的那些对象, 复制的只是"指向同一个东西的引用", 本质上还是共享的同一个;于是当我去改那个"共享的内核"时, 两边都变了——因为它们在那一层, 从来就没真正分开过。这让我领悟到一个关于"分离的深度与共享的内核"的深刻认知:"把两个东西分开/复制成独立的两份", 是有"深度"的: 它可能只分离了表层, 也可能贯穿到底; 而在分层/嵌套的结构里, 表层的分离不等于深层的分离——深处可能仍共享着同一个内核;危险就在于, 表层的分离会给我们一种"已经彻底独立"的错觉, 让我们放心地去改动, 却没意识到自己正在改的, 是一个还被对方共享着的、牵一发动全身的东西;所以当我需要"真正独立、互不影响"时, 必须确认这种分离贯穿到了我会去改动的每一层, 而不是停留在表面; 而更省心的办法, 是干脆不去改动那些可能被共享的东西(用不可变、用副本、用新建), 从根上消除"共享内核被意外波及"的可能。这给了我一种看待"一切'复制、分离、隔离某个有内部结构之物'之事"时的清醒:每当我"复制/分离"一个有内部结构的东西、并打算独立改动它时, 要追问"这个分离是只到了表层, 还是贯穿到了我会改动的深处?它们内部会不会还共享着同一个内核?我改这一层, 会不会动到对方?"——对需要真正独立的, 确保分离贯穿到底(深拷贝); 或干脆用不可变/不改原件的方式, 避免共享内核被波及;"分清分离的深度、确保独立贯穿到要改动的每一层", 是用对拷贝、也是做对一切'隔离与复制'的关键。认清浅拷贝只独立外层而内层共享、改共享的嵌套对象会动原件、要真正独立用 deepcopy——这,是我用一次改副本动原件的事故,换来的、关于 Python、也关于如何看待分离深度与共享内核的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次 copy() 一个嵌套结构、准备放心改副本时,先想想"这只独立了外层吧?里面那层是不是还和原件共享着?我要不要 deepcopy?",并在需要时换上 copy.deepcopy,那我对着那个"改副本却动了原件"的诡异现象折腾的大半天,就值了。
—— 别看了 · 2026