一段用浅拷贝复制配置模板的 Python 代码,我改了副本里的一个嵌套列表,结果把原始模板和其他所有副本一起改了:一次浅拷贝陷阱的深度复盘

基于一个嵌套配置模板生成多份各自独立的配置,用 copy.copy 复制后改一份,结果原模板和其他所有副本里的那个嵌套列表全跟着变了。根因是 copy.copy/dict.copy/list

一段用浅拷贝复制配置模板的 Python 代码,我改了副本里的一个嵌套列表,结果把原始模板和其他所有副本一起改了:一次浅拷贝陷阱的深度复盘

那个 bug 诡异得让我以为变量被""了:我有一个嵌套的配置模板(一个 dict,里面套着 list 和子 dict),需要基于它生成好几份"各自独立"的配置,每份在模板基础上改一点。我很自然地用 copy.copy()(或 dict.copy())把模板复制一份、再改。可结果让我大跌眼镜:我只改了某一份副本里的一个嵌套列表(往里 append 了一项),结果——原始模板、以及其他所有副本里的那个列表,全都跟着多了这一项!我明明复制过了啊,怎么改一份会影响全部?我对着这段"明明拷贝了"的代码调试了大半天,才终于想起 Python 拷贝那个最经典的陷阱,后背发凉:copy.copy()dict.copy()list[:] 这些都是浅拷贝(shallow copy)。浅拷贝只复制了最外层的那个容器,而容器里面的元素(尤其是嵌套的 list、dict 这些可变对象),复制的只是它们的"引用",并没有真正复制它们本身。也就是说,我的副本和原模板,各自有了一个独立的外层 dict,但它们内部那个嵌套的 list,指向的还是同一个 list 对象!于是当我往副本的那个嵌套 list 里 append 时,改的其实是那个被大家共享的、同一个 list——原模板和其他副本看到的是同一个,自然全都""了。问题的根,是浅拷贝"只拷一层",而我以为它"拷贝了整个嵌套结构"。这篇就把这次"浅拷贝陷阱、嵌套共享"的坑,从头到尾复盘一遍。

故障现场:浅拷贝嵌套结构,内层还是共享的

问题代码,是一个用浅拷贝复制嵌套 dict 的写法:

import copy

# 一个嵌套的配置模板
template = {
    "name": "default",
    "tags": ["a", "b"],          # 嵌套的 list(可变对象)
    "limits": {"max": 100},      # 嵌套的 dict(可变对象)
}

# ✗ 出问题: 用浅拷贝复制模板
config1 = copy.copy(template)    # 或 template.copy() / dict(template)
config1["name"] = "config1"      # 改最外层的不可变值: 没问题, 只影响config1
config1["tags"].append("c")      # ✗ 改嵌套的list: 影响了所有人!

print(template["tags"])          # ['a', 'b', 'c']  ✗ 原模板也被改了!
print(config1["tags"])           # ['a', 'b', 'c']

config2 = copy.copy(template)
print(config2["tags"])           # ['a', 'b', 'c']  ✗ 新副本一来就带着'c'!

# 为什么:
# - copy.copy() 是【浅拷贝】: 只复制最外层的dict(创建了一个新dict);
# - 但新dict里的 "tags" 和 "limits", 复制的只是【引用】——
#   config1["tags"] 和 template["tags"] 【指向同一个list对象】!
# - → config1["tags"].append("c") 改的是那个【共享的同一个list】 → 所有引用它的都"看到"变化。
# - 而 config1["name"]="config1" 没问题: 因为是给config1这个【新dict】的key重新赋值(不影响template)。

# 浅拷贝 vs 深拷贝:
# - 浅拷贝(copy.copy / dict.copy / list[:] / {**d}): 只复制最外层, 嵌套的可变对象仍共享引用;
# - 深拷贝(copy.deepcopy): 递归地复制所有层级, 副本和原对象【完全独立】, 互不影响。

# 关键: 浅拷贝只复制一层, 嵌套的可变对象还是共享的; 改副本的嵌套元素会影响原对象和其他副本;
#       要完全独立的副本(尤其嵌套结构), 得用深拷贝 copy.deepcopy。

第一次看清这个"共享的嵌套 list"时,我又懊恼又恍然:"我以为复制就是复制了一整份,原来它只复制了'外壳',里面的东西还是大家共用的?"这个坑最迷惑人的地方在于它的"部分有效":改副本里最外层的、不可变的值(如 config1["name"])是正常的、独立的(因为那是给新 dict 的 key 重新赋值);只有改嵌套的、可变的对象(往嵌套 list 里 append、改嵌套 dict)时,才会""到原对象这种"改外层没事、改内层就串"的不一致,让人极难一眼看穿——你测试时可能只改了外层值,觉得"拷贝没问题",直到某次改了嵌套对象才爆雷。下面就来拆解,浅拷贝和深拷贝的区别。

第一件事:搞懂浅拷贝、深拷贝,以及引用语义

我认真重学了 Python 的拷贝机制,才彻底理解这个坑。

浅拷贝 vs 深拷贝, 以及背后的"引用语义"

【核心: 浅拷贝只复制最外层、嵌套对象仍共享引用; 深拷贝递归复制所有层、完全独立; 根源是Python的变量是"引用"】

1. 先理解: Python的变量是"引用(指向对象)", 不是"装着值的盒子"
   - 赋值 b = a 不复制对象, 只是让 b 和 a 【指向同一个对象】;
   - 所以 b 改了可变对象, a 也"看到"(它们是同一个)。

2. 浅拷贝(shallow copy):
   - copy.copy(x) / dict.copy() / list[:] / {**d} / list(x) 等;
   - 它创建一个【新的外层容器】, 但容器里的元素只复制【引用】;
   - → 外层是新的(独立), 但嵌套的可变对象(list/dict)和原来【共享同一个】;
   - → 改外层结构(增删外层key/重新赋值)不影响原; 但改嵌套对象会影响原。

3. 深拷贝(deep copy):
   - copy.deepcopy(x);
   - 它【递归地】复制每一层的每一个对象——嵌套的list/dict也都复制出新的;
   - → 副本和原对象【在所有层级上都完全独立】, 改副本的任何部分都不影响原。

4. 什么时候浅拷贝够 / 什么时候要深拷贝:
   - 结构是"扁平的"(没有嵌套可变对象, 或只读嵌套): 浅拷贝够用、还快;
   - 结构有"嵌套的可变对象", 且你要【独立地修改副本的嵌套部分】: 必须深拷贝;
   - 深拷贝的代价: 慢、占内存(要复制整棵结构); 别滥用(扁平结构没必要)。

5. 这是一个跨语言的普遍主题:
   - "复制一个对象"到底是复制"引用/外壳"还是"完整内容", 在各语言都存在(浅拷贝/深拷贝、
     值语义/引用语义); 不分清就会踩"改一个影响另一个"的坑(同Go slice共享、JS对象引用)。

一句话: Python变量是引用; 浅拷贝只复制最外层、嵌套可变对象仍共享(改它会串), 深拷贝递归复制全部、
   完全独立; 嵌套结构要独立副本用copy.deepcopy, 扁平结构浅拷贝够(深拷贝有性能代价别滥用)。

这套机制,是整个坑的根。先理解:Python 的变量是"引用"(指向对象)、不是"装值的盒子",b = a 不复制对象、只是让 b 和 a 指向同一个。浅拷贝(copy.copy/dict.copy/list[:]/{**d}):创建一个新的外层容器,但容器里的元素只复制引用,所以外层是新的(独立),但嵌套的可变对象和原来共享同一个——改外层结构不影响原、但改嵌套对象会影响原。深拷贝(copy.deepcopy):递归地复制每一层每一个对象,副本和原对象在所有层级上完全独立。何时用哪个?扁平结构(无嵌套可变对象)浅拷贝够用且快;有嵌套可变对象且要独立修改副本的嵌套部分,必须深拷贝;深拷贝慢、占内存,别滥用这是跨语言的普遍主题:"复制对象"到底是复制引用/外壳还是完整内容,各语言都存在(浅/深拷贝、值/引用语义),不分清就踩"改一个影响另一个"的坑(同 Go slice 共享、JS 对象引用)一句话:Python 变量是引用;浅拷贝只复制最外层、嵌套可变对象仍共享(改它会串),深拷贝递归复制全部、完全独立;嵌套结构要独立副本用 copy.deepcopy,扁平结构浅拷贝够(深拷贝有性能代价别滥用)。

第二件事:正解——嵌套结构要独立副本用 deepcopy,或重建结构

搞懂了原理,正解就清晰了:需要"完全独立"的嵌套副本时用 copy.deepcopy;或显式重建结构;或优先用不可变的数据结构从根上避免共享修改的问题

import copy

template = {"name": "default", "tags": ["a", "b"], "limits": {"max": 100}}

# ====== 正解一: 用 deepcopy 得到完全独立的副本 ======
config1 = copy.deepcopy(template)   # ★ 递归复制所有层级
config1["tags"].append("c")         # 只改config1自己的tags
print(template["tags"])             # ['a', 'b']  ✓ 原模板没被改!
print(config1["tags"])              # ['a', 'b', 'c']  ✓ 只有config1变了
# → deepcopy: 副本和原对象在所有层级都独立, 改副本任何部分都不影响原。

# ====== 正解二: 显式重建嵌套部分(知道结构时, 更可控/更快) ======
config2 = {
    **template,                     # 浅展开外层
    "tags": list(template["tags"]),       # ★ 显式复制嵌套list
    "limits": dict(template["limits"]),   # ★ 显式复制嵌套dict
}
# → 手动把要独立的嵌套部分复制一份; 比deepcopy更可控、对扁平+少量嵌套更高效。

# ====== 正解三(治本): 用不可变数据结构, 从根上避免"改了共享对象" ======
# - 用 tuple 代替 list(只读)、frozenset、namedtuple、dataclass(frozen=True)、或不可变库;
# - 不可变对象"改"它时只能产生新对象, 不会就地修改共享的那个 → 没有"串改"问题;
# - (这也是函数式编程推崇不可变的原因之一: 不可变 = 没有共享可变状态的麻烦)
# ====== 拷贝方式速记 ======
# 浅拷贝(只复制外层, 嵌套共享):
#   copy.copy(x)、dict.copy()、list[:]、list(x)、{**d}、[*lst]
# 深拷贝(递归复制所有层, 完全独立):
#   copy.deepcopy(x)

# ====== 选型 ======
# - 扁平结构(无嵌套可变对象): 浅拷贝够用、快;
# - 嵌套结构 + 要独立修改副本的嵌套部分: deepcopy(或显式重建嵌套部分);
# - 频繁拷贝大结构: 考虑用不可变数据结构, 避免拷贝(共享只读是安全的);
# - 拿不准会不会改嵌套: 倾向deepcopy更安全(代价是慢, 权衡)。

# ====== 排查口诀 ======
# "我改了A, B却跟着变了" / "改了副本, 原对象也变了" → 八成是【共享了同一个可变对象】
#   (浅拷贝没拷深、或直接赋值没拷贝) → 检查是不是该deepcopy。

# 核心: 嵌套结构要完全独立的副本, 用copy.deepcopy或显式重建嵌套部分(别用浅拷贝); 扁平结构浅拷贝够;
#   优先考虑不可变数据结构避免共享修改; "改一个影响另一个"先怀疑共享了同一个可变对象。

修复的核心,是"要独立就真正复制到底,或干脆用不可变避免共享"正解一:用 copy.deepcopy——递归复制所有层级,副本和原对象在所有层级都独立,改副本任何部分都不影响原正解二:显式重建嵌套部分——{**template, "tags": list(...), "limits": dict(...)},知道结构时更可控、对少量嵌套更高效正解三(治本):用不可变数据结构——tuple/frozenset/namedtuple/frozen dataclass,改它只能产生新对象、不会就地修改共享的那个,从根上没有"串改"问题(函数式推崇不可变的原因之一)选型:扁平用浅拷贝、嵌套且要独立改用 deepcopy、频繁拷贝大结构考虑不可变、拿不准倾向 deepcopy排查口诀:"改了 A 但 B 跟着变""改副本原对象也变"八成是共享了同一个可变对象归根结底:嵌套结构要完全独立的副本用 copy.deepcopy 或显式重建嵌套部分(别用浅拷贝);扁平结构浅拷贝够;优先考虑不可变数据结构避免共享修改;"改一个影响另一个"先怀疑共享了同一个可变对象。

第三件事:Python 引用与可变性相关的其他常见坑

排查后我把 Python 引用/可变性相关的其他常见坑也系统梳理了一遍。

Python 引用 / 可变性的其他常见坑

# 1. 浅拷贝嵌套共享(本文): 改副本嵌套对象影响原。→ deepcopy/重建/不可变。

# 2. 可变默认参数: def f(x, lst=[]) 的lst在所有调用间共享。→ 默认用None, 函数内再建。

# 3. 可变类变量被实例共享: class C: items=[] 所有实例共享。→ 在__init__里 self.items=[]。

# 4. 直接赋值不是拷贝: b = a 只是引用同一对象, 改b就是改a。→ 要副本就显式拷贝。

# 5. 函数内修改传入的可变参数: 函数改了传进来的list/dict, 调用方的也变了。→ 注意副作用/先拷贝。

# 6. 用 list * n 造二维列表: [[0]*3]*3 的三行是同一个list。→ 用列表推导 [[0]*3 for _ in range(3)]。

# 7. 字典/集合的key用可变对象: list不能当key; 可变对象当key语义混乱。→ 用不可变(tuple)。

# 8. is vs ==: is比身份(是不是同一对象), ==比值; 别用is比较值(小整数缓存会迷惑你)。

# 共同根源: Python里变量是"引用", 多个变量可能指向同一个【可变对象】;
#   "拷贝/赋值/传参"时若没意识到"它们可能共享同一个可变对象", 改一个就会意外影响另一个。

# 核心: 理解Python变量是引用、可变对象会被共享; 要独立副本用deepcopy/重建; 警惕可变默认参数/类变量;
#   函数改传入的可变参数有副作用; 优先用不可变对象; "改一个影响另一个"先查共享。

排查让我把引用/可变性的其他坑也梳理清了。一、浅拷贝嵌套共享(本文)。二、可变默认参数(默认用 None)。三、可变类变量被实例共享(__init__ 里建)。四、直接赋值不是拷贝五、函数内改传入的可变参数(副作用)。六、list * n 造二维列表(用列表推导)。七、用可变对象当字典 key八、is vs ==它们的共同根源是:Python 里变量是"引用",多个变量可能指向同一个可变对象;"拷贝/赋值/传参"时若没意识到"它们可能共享同一个可变对象",改一个就会意外影响另一个核心是:理解 Python 变量是引用、可变对象会被共享;要独立副本用 deepcopy/重建;警惕可变默认参数/类变量;函数改传入可变参数有副作用;优先用不可变对象;"改一个影响另一个"先查共享下面这张图,是这次浅拷贝坑的成因与解法:

第四件事:赋值 / 浅拷贝 / 深拷贝对比表

这次踩坑后,我把"赋值、浅拷贝、深拷贝"三者的差异整理成一张表。

操作 外层 嵌套可变对象
b = a(赋值) 同一个(共享) 同一个(共享)
copy.copy(浅拷贝) 新的(独立) 同一个(共享)
copy.deepcopy(深拷贝) 新的(独立) 新的(独立)
list[:] / dict.copy() 新的(独立) 同一个(共享)=浅
{**d} / [*lst] 新的(独立) 同一个(共享)=浅

这张表把三种"复制"的层次钉清了。核心是:从"赋值"到"浅拷贝"到"深拷贝",其实是"复制的深度"在递进——赋值复制 0 层(完全共享)、浅拷贝复制 1 层(外层独立、内层共享)、深拷贝复制所有层(完全独立);选哪个,取决于你需要"独立"到哪一层它给我的最大启发是:"独立"和"共享"不是非黑即白的两极,而是一个有"程度/层次"的光谱——同一份数据,可以在外层独立、内层共享(浅拷贝),也可以完全独立(深拷贝);关键是想清楚"我到底需要在哪些层级上独立、哪些层级上可以共享";很多 bug 源于"以为是完全独立,实际只独立了一层"或"以为复制了,实际只是共享"——对"独立程度"的预期和现实不符这让我对"复制/共享"有了更精细的认识:处理数据时,要精确地知道"这次操作产生的是共享、浅拷贝、还是深拷贝",以及"我的需求需要哪一种"——不要笼统地以为"复制了就独立了";"明确每次复制的'深度'、并匹配需求",是避免共享/独立类 bug 的关键;尤其在拷贝嵌套结构、传递参数、缓存数据时,要想清楚"我要的是真独立还是可共享"把复制理解成有层次的光谱、精确匹配独立程度的需求——是这个坑带给我的认知。

第五件事:浅拷贝坑背后的"可变性"反思

这次的坑,根子是"可变对象被共享"。我把"可变"带来的麻烦和"不可变"的好处对比成表。

维度 可变对象(list/dict) 不可变对象(tuple/str/frozenset)
被共享时 改一处, 处处变(本文的坑) 不能改, 永远安全
能否当字典key 不能(list)
并发安全 要加锁 天然安全
拷贝 常需deepcopy防共享 共享即可(反正改不了)
"改"它 就地修改(影响共享者) 产生新对象(不影响别人)

这张表道出了"可变 vs 不可变"的深层差异。核心是:本文这个坑,以及可变默认参数、类变量共享、并发数据竞争等一大堆问题,根子都是同一个:"可变对象"被多处共享、而某处"就地修改"了它,于是影响了所有共享它的地方;而"不可变对象"从根上没有这个问题——它不能被就地修改,""它只会产生一个新对象,绝不会影响别人它给我的深刻启发是:"不可变性(immutability)"是解决一大类问题的釜底抽薪之策——共享修改的坑(本文)、并发数据竞争、意外的副作用、难追踪的状态变化……这些问题的共同根源是"共享的可变状态",而不可变性直接消灭了"可变"这一半;"没有可变状态,就没有'谁改了它'的问题"——这正是函数式编程、以及现代很多语言/框架推崇不可变的根本原因这给了我一种数据设计的倾向:在合适的地方优先选用不可变数据——能用 tuple 就别用 list、能用 frozen dataclass 就别用可变对象、共享的数据尽量设计成只读;"默认不可变、需要可变时才可变",能从源头避开一大类由"共享可变状态"引起的、最隐蔽难查的 bug;虽然不可变有"改就得建新对象"的代价,但它换来的"安全、可预测、易推理",往往非常值得认清共享可变状态是一大类 bug 的根、优先选用不可变数据——是这个浅拷贝坑带给我的更深认知。

第六件事:复制一个对象时,我现在的判断习惯

现在每当我要"复制"一个对象,我都会按这张图先想清楚:

这张图的精髓,是"先想要不要独立、有没有嵌套可变对象,再选赋值/浅拷贝/深拷贝"可共享/只读就直接共享;要独立副本则看有没有嵌套可变对象:扁平用浅拷贝、有嵌套且要改用deepcopy 或重建嵌套部分;频繁拷贝考虑不可变结构这套习惯,让我从"复制就 copy 一下"变成了"复制先想独立程度和嵌套"——核心始终是:明确需要独立到哪一层,嵌套结构要独立就用 deepcopy,优先考虑不可变。

我立下的几条规矩

这场"浅拷贝、嵌套共享、改一个串全部"的事故,换来了我写 Python 时,刻进骨子里的几条铁律:

  1. Python 变量是引用,可变对象会被共享。赋值不复制对象、只共享引用。
  2. 浅拷贝只复制最外层,嵌套可变对象仍共享。改嵌套对象会影响原。
  3. 要完全独立的嵌套副本,用 copy.deepcopy。或显式重建嵌套部分。
  4. 扁平结构浅拷贝够用。deepcopy 慢、占内存,别滥用。
  5. "改一个影响另一个"先怀疑共享了同一可变对象。排查共享。
  6. 优先用不可变数据结构。从根上避免共享可变状态的麻烦。
  7. 明确复制的"深度"、匹配独立程度的需求。别笼统以为"复制了就独立"。

写在最后

回头看,这场由"浅拷贝只拷了一层"引发的、改一份副本却串改了全部的事故,真正教给我的,远不止"嵌套结构要用 deepcopy"这一个技巧。它让我对"'复制'这个我们以为最简单的操作,底下藏着'到底复制了什么'的深刻问题",有了一次刻骨的体会。我栽跟头,根源在于我对"复制"有一个过于天真的心智模型:在我的直觉里,"复制一个东西"就是"得到一个一模一样、但完全独立的另一个"——就像复印一份文件,复印件和原件互不相干。可在 Python(以及大多数有"引用"的语言)里,"复制"远没这么简单:一个嵌套对象,就像一个"盒子里装着指向其他盒子的纸条";"浅复制这个盒子",复制的是盒子本身和里面那些纸条(引用),而纸条指向的那些'其他盒子',并没有被复制——新盒子和旧盒子里的纸条,指向的还是同一批盒子;我以为我复印了"整个嵌套的文件柜",实际只复印了"最外层的目录页(还指向同一批内页)"这让我领悟到一个理解"引用型语言"的核心认知:在变量是"引用"的语言里,要时刻在脑子里区分"引用(指向对象的纸条)"和"对象本身(被指向的盒子)"这两个层次——"复制一个引用"(让两张纸条指向同一个盒子)和"复制一个对象"(造一个新盒子)是完全不同的两件事;赋值、浅拷贝、深拷贝,本质就是"哪些层次上复制了'对象'、哪些层次只复制了'引用'"的区别;看不清"引用"和"对象"的分野,就会在"复制、共享、修改"上反复踩坑这给了我一种处理引用的清醒:每当涉及"复制、传递、共享"一个对象时,都要在脑子里问:"我现在操作的、传递的,是'引用'还是'对象本身'?这个引用和别处的引用,指向的是同一个对象吗?"——把"引用"和"对象"这两层清晰地分开,你就能准确预判"改这个会不会影响那个";"看清引用与对象的分野",是驾驭一切引用型语言(Python/Java/JS/Go...)的一项底层基本功看清"引用"与"对象本身"的分野、理解复制的是引用还是对象——这,是我用一次浅拷贝串改的事故,换来的、关于 Python、也关于如何理解一切引用型语言的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次复制一个嵌套结构时,先想一句"这是浅拷贝吧?里面的嵌套对象还是共享的",转而用上 deepcopy,那我对着那些被串改的配置排查的这大半天,就值了。

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

一个工具描述写得含含糊糊的 AI Agent,在该查订单时却去退了款、参数还填错,把工具用得乱七八糟:一次工具定义不清的深度复盘

2026-6-2 18:30:43

技术教程

一个把对象方法直接作为回调传给 setTimeout 的写法,执行时 this 变成了 undefined、访问 this 的属性全报错:一次 JavaScript this 绑定丢失的深度复盘

2026-6-2 18:40:24

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