Python 的浅拷贝 vs 深拷贝(含嵌套可变对象示例与踩坑场景)

发布于:2025-08-11 ⋅ 阅读:(17) ⋅ 点赞:(0)

很多人第一次被“拷贝”坑,是因为“看起来复制了”,但一改里面的内容,另一个也跟着变。根源在于:Python 的“浅拷贝”和“深拷贝”处理“嵌套可变对象”的方式不同。下面用通俗话、配上可直接运行的代码,把这个问题讲透。

一句话区分

  • 浅拷贝(copy.copy / list.copy / 切片[:]):只“复制外壳”,里面嵌套的对象仍然“共用同一份”,所以改里层会“互相影响”。
  • 深拷贝(copy.deepcopy):外壳+里面每一层都复制一份新对象,彼此彻底独立,互不影响。

小结:是否“会串味”,关键看有没有“嵌套的可变对象”(比如列表、字典、集合、自定义可变对象)。只有深拷贝能“层层分家”。

赋值不是拷贝

别把“赋值 a = b”当复制,这只是让 a 和 b 指向同一个对象,改谁都影响另一个。

a = [1, 2]
b = a       # 只是多了一个名字指向同一对象
b.append(3)
print(a)    # [1, 2, 3]  两个都变了,因为根本不是拷贝

代码直观演示:浅拷贝会“串味”

场景:列表里套列表(嵌套可变对象),用浅拷贝复制外层。

import copy

a = [[1, 2], [3, 4]]
b = copy.copy(a)     # 浅拷贝;等价于 b = a[:] 或 b = list(a)

b[^0][^0] = 99         # 改“里层”的列表元素
print("a:", a)       # a: [[99, 2], [3, 4]]  a也被改到
print("b:", b)       # b: [[99, 2], [3, 4]]

为什么会这样?浅拷贝只复刻了“外层列表”的壳,但里面的两个内层列表仍是“同一对象”的两个引用,改里层当然会互相影响。

再看另一个对比:改外层引用不串味

import copy

a = [[1, 2], [3, 4]]
b = copy.copy(a)

a[^0] = ["X", "Y"]    # 改“外层”的第0个元素,让它指向一个新列表
print("a:", a)       # a: [['X', 'Y'], [3, 4]]
print("b:", b)       # b: [[1, 2], [3, 4]]   这次 b 没变

原因:浅拷贝“外层的元素引用”是分开的,替换外层引用不影响另一份;但如果深入到“里层对象内部”去改,就动到了共享的那一份。

代码直观演示:深拷贝彻底“分家”

import copy

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)  # 深拷贝

b[^0][^0] = 88
print("a:", a)        # a: [[1, 2], [3, 4]]  不受影响
print("b:", b)        # b: [[88, 2], [3, 4]]

deepcopy 会递归地把每层都复制一个新的,因此各自独立,互不影响。

常见踩坑清单

  • 以为 list.copy() 或切片是“完全复制”,结果在“列表套列表/字典套字典”时,改里层互相污染(典型面试坑)。
  • 以为“赋值”是复制 a=b,结果两个变量是同一对象,怎么改都一起变(初学者常见)。
  • 配置模板/默认参数写成嵌套结构,用浅拷贝衍生新配置,一改内部列表/字典,全局模板也被改坏。
  • 带“循环引用”的结构用浅拷贝会破坏关系,用深拷贝能正确处理;deepcopy 内部对递归/环结构做了特殊处理。

什么时候用浅拷贝更合适?

  • 外层需要一份新容器,但“里面全是不可变对象”(如字符串、数字、元组),这时浅拷贝够用、也更快更省内存。
  • 只会替换外层引用(比如重新绑定 a=…),不会去改里层对象本身,可以用浅拷贝。
  • 性能敏感、数据巨大,而你能确定只在外层动刀,浅拷贝胜在轻量。

什么时候必须用深拷贝?

  • 内部包含可变对象,且“会修改里层内容”,又不想影响原对象,就用 deepcopy。
  • 复杂对象图(列表/字典深度嵌套、对象彼此引用),需要完全隔离副作用,deepcopy 省心。
  • 并发或多进程场景,为了避免共享可变状态导致的竞态,常用深拷贝隔离数据(结合实际权衡性能)。

进阶注意点与优化思路

  • deepcopy 较慢,会做递归与“去重/环”记账,规模大时要评估成本;能用浅拷贝就别深拷贝,或只在必要层级手动复制(定向复制)。[^4]
  • 对不可变对象(如 int/str/tuple)没必要深拷贝,Python 可能还会重用小整数/常量对象(实现细节,不影响语义)。
  • 自定义类可用 copy/deepcopy 定制拷贝行为,避免无意义的深度复制或遗漏关键状态。

最实用的判断准则

  • 只复制外壳、里层不可变或不动里层:浅拷贝。
  • 有嵌套可变对象,且要改里层:深拷贝。
  • 不要把赋值当拷贝。

常用代码备忘

import copy

# 浅拷贝几种方式(外层新容器,里层共享)
b = copy.copy(a)
b = a[:]           # 切片
b = list(a)        # 构造函数(外层)

# 深拷贝(外层+里层全部新对象)
b = copy.deepcopy(a)

综合示例:配置模板复制与安全修改

import copy

# 模板里有嵌套可变对象
TEMPLATE = {
    "name": "job",
    "steps": [
        {"op": "load", "params": {"paths": ["a.csv", "b.csv"]}},
        {"op": "transform", "params": {"dropna": True}},
    ],
    "tags": ["daily", "etl"],
}

# 1) 浅拷贝:外层复制,里层共享(危险)
job1 = TEMPLATE.copy()             # 或 copy.copy(TEMPLATE)
job1["steps"][^0]["params"]["paths"].append("c.csv")

print("浅拷贝影响模板?", TEMPLATE["steps"][^0]["params"]["paths"])
# 会看到模板也变了,因为 steps 内部字典/列表是共享的。[^3][^7][^20]

# 2) 深拷贝:完全独立(安全)
job2 = copy.deepcopy(TEMPLATE)
job2["steps"][^0]["params"]["paths"].append("d.csv")

print("深拷贝不影响模板:", TEMPLATE["steps"][^0]["params"]["paths"])
# 模板不变,job2 自己有 d.csv。[^1][^3][^12]

一个“奇葩”案例:自引用结构

当容器里引用自己(循环引用)时,浅拷贝会破坏这种关系;deepcopy 能保留结构的自洽性。

import copy, pprint

x = [^1]
x.append(x)       # x 指向自己: [1, [...]]
shallow = copy.copy(x)
deep = copy.deepcopy(x)

pprint.pp(x)        # [1, <Recursion on list ...>]
pprint.pp(shallow)  # [1, [1, <Recursion on list ...>]]  第二个元素指回原x
pprint.pp(deep)     # [1, <Recursion on list ...>]       结构被正确复制

这个例子说明 deepcopy 会正确处理循环引用,保持拷贝后的结构自洽;浅拷贝只是拷“引用”,导致关系指回了原对象。


核心记忆点:

  • 浅拷贝:新外壳、里层共享;改里层会“串味”。适用于里层不可变或只换外层引用。
  • 深拷贝:外壳+里层全复制,互不影响;适用于需要修改里层或复杂结构隔离副作用。
  • 赋值不是拷贝,谨慎对待嵌套可变对象,模板/默认配置场景尤其容易踩坑。

网站公告

今日签到

点亮在社区的每一天
去签到