我把一个嵌套的字典拷贝了一份、想改副本而不动原件,结果一改副本里层的数据、原件竟然也跟着变了,排查半天才发现我用的拷贝只复制了最外层、里面那层还是和原件共享的同一个对象的深度复盘

我有个嵌套的数据结构(字典里的值又是列表/字典),要在不影响原件的前提下拷一份出来改改。我很自然用 copy()(或切片

我把一个嵌套的字典拷贝了一份、想改副本而不动原件,结果一改副本里层的数据、原件竟然也跟着变了,排查半天才发现我用的拷贝只复制了最外层、里面那层还是和原件共享的同一个对象的深度复盘

这是一次让我对"'复制一份'到底复制到了哪一层"有了刻骨认知的事故。我有个嵌套的数据结构(一个字典,里面的值又是列表/字典),需要在不影响原件的前提下,拷一份出来改改、做点试探性的处理。我很自然地用 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 时,刻进骨子里的几条铁律:

  1. copy()/dict()/list()/[:]/{**d}/[*l] 都是浅拷贝:只独立最外层,嵌套的可变对象副本和原件共享同一个。
  2. 浅拷贝下改外层不互相影响,但改嵌套对象内部会互相影响(那是共享的同一个对象)。
  3. 嵌套结构 + 会改内层 + 要真正独立 → 用 copy.deepcopy 递归复制所有层级。
  4. 只需独立外层、或元素都不可变 → 浅拷贝就够;deepcopy 慢占内存,别滥用。
  5. 赋值 b = a 不是拷贝,只是给同一对象起别名,改 b 就是改 a。
  6. 更省心:用函数式风格(不改原件、返回新结构)或不可变类型(tuple/frozenset)从根上避坑。
  7. 这与可变默认参数、列表乘法共享引用是同一内核:多个名字指向同一可变对象,改一个动另一个。

附:一段把"浅拷贝共享内层、深拷贝彻底独立"摆清楚的小实验

这是我后来写的一段小实验,把赋值、浅拷贝、深拷贝对嵌套结构的影响并排跑出来——它把"分离到第几层"这个抽象的事变成了眼见为实的对比,现在我也常拿它给同事讲清这个坑:

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——副本和原件的内层是同一个对象,所以改副本内层、原件也变;只有深拷贝那行 isFalse、原件 items 纹丝不动,才是真正彻底独立的两份。跑完这段我才真正在脑子里刻下:判断两份数据是不是真独立,别看它俩外层是不是两个对象,要看我会去改的那一层、那个嵌套对象,is 判断是不是同一个;只要还是同一个,改一个就会动另一个。is 把"共享的内核"亲手验出来、看着它从 True 变成 False,比记任何"嵌套要用 deepcopy"的口诀都更让我对"分离的深度"心里有数。

写在最后

回头看,这场由"浅拷贝"引发的"改副本动原件"事故,真正教给我的,远不止"嵌套结构用 deepcopy"这一个技巧。它让我对"当我们说'复制一份'、'分开'、'独立'时, 这种'分离'常常只是'表层的分离'——表面上是两个东西了, 可它们的'内核'、'深处依赖的那部分', 可能依然是同一个、共享的; 而我们误以为已经彻底分开、可以各自折腾互不影响, 一旦动了那个共享的内核, 才发现牵一发而动全身",有了一次刻骨的体会。我栽跟头,是因为我把'表层的分离'当成了'彻底的、贯穿到底的分离'——我看到副本和原件是两个不同的字典(外层确实分开了), 就以为它俩从里到外都是独立的两份;我没意识到, 这个"分开"只发生在最外层; 它们内部嵌套的那些对象, 复制的只是"指向同一个东西的引用", 本质上还是共享的同一个;于是当我去改那个"共享的内核"时, 两边都变了——因为它们在那一层, 从来就没真正分开过这让我领悟到一个关于"分离的深度与共享的内核"的深刻认知:"把两个东西分开/复制成独立的两份", 是有"深度"的: 它可能只分离了表层, 也可能贯穿到底; 而在分层/嵌套的结构里, 表层的分离不等于深层的分离——深处可能仍共享着同一个内核;危险就在于, 表层的分离会给我们一种"已经彻底独立"的错觉, 让我们放心地去改动, 却没意识到自己正在改的, 是一个还被对方共享着的、牵一发动全身的东西;所以当我需要"真正独立、互不影响"时, 必须确认这种分离贯穿到了我会去改动的每一层, 而不是停留在表面; 而更省心的办法, 是干脆不去改动那些可能被共享的东西(用不可变、用副本、用新建), 从根上消除"共享内核被意外波及"的可能这给了我一种看待"一切'复制、分离、隔离某个有内部结构之物'之事"时的清醒:每当我"复制/分离"一个有内部结构的东西、并打算独立改动它时, 要追问"这个分离是只到了表层, 还是贯穿到了我会改动的深处?它们内部会不会还共享着同一个内核?我改这一层, 会不会动到对方?"——对需要真正独立的, 确保分离贯穿到底(深拷贝); 或干脆用不可变/不改原件的方式, 避免共享内核被波及;"分清分离的深度、确保独立贯穿到要改动的每一层", 是用对拷贝、也是做对一切'隔离与复制'的关键认清浅拷贝只独立外层而内层共享、改共享的嵌套对象会动原件、要真正独立用 deepcopy——这,是我用一次改副本动原件的事故,换来的、关于 Python、也关于如何看待分离深度与共享内核的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次 copy() 一个嵌套结构、准备放心改副本时,先想想"这只独立了外层吧?里面那层是不是还和原件共享着?我要不要 deepcopy?",并在需要时换上 copy.deepcopy,那我对着那个"改副本却动了原件"的诡异现象折腾的大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我给 AI Agent 派了个需要好几步才能完成的复杂任务,它倒是很积极地一上来就埋头开干,结果做着做着就跑偏了、漏了关键步骤、还在几个动作之间原地打转,排查半天才明白它压根没先把任务拆解、规划一下就硬上的深度复盘

2026-6-3 6:20:45

技术教程

我想快速造一个长度为 5、每项都初始化好的数组,顺手写了 new Array(5).map,结果 map 里的函数一次都没执行、拿到的还是一个全是空的数组,排查半天才发现 new Array(5) 造出来的根本不是 5 个 undefined、而是 5 个会被 map 跳过的空位的深度复盘

2026-6-3 6:33:26

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索