为什么更新“浅”复制的字典不会更新“原始”的字典?

545

在阅读 dict.copy() 的文档时,它说它会创建一个字典的浅拷贝。我正在跟随的书籍(Beazley's Python Reference)也是如此:

m.copy() 方法将映射对象中包含的项进行浅拷贝,并将它们放入新的映射对象中。

考虑以下内容:

>>> original = dict(a=1, b=2)
>>> new = original.copy()
>>> new.update({'c': 3})
>>> original
{'a': 1, 'b': 2}
>>> new
{'a': 1, 'c': 3, 'b': 2}

所以我假设这会更新original的值(并添加'c': 3),因为我正在进行浅拷贝。就像你对列表做的那样:

>>> original = [1, 2, 3]
>>> new = original
>>> new.append(4)
>>> new, original
([1, 2, 3, 4], [1, 2, 3, 4])

这个按预期运行。

由于两者都是浅复制,为什么 dict.copy() 不像我期望的那样工作呢?或者我的浅复制和深复制的理解有误?


7
有趣的是他们没有解释“shallow”的含义。内部知识,眨眼。第一层中的字典和键只是一个副本,而嵌套在该第一层中的字典是引用,例如无法在循环中删除。因此,在这种情况下,Python的dict.copy()既不实用也不直观。谢谢您的提问。 - gseattle
由于两者都是浅拷贝 - 这就是问题所在。第二个例子不是拷贝,无论是浅拷贝还是深拷贝。 - Karl Knechtel
7个回答

1269

所谓的“浅复制”,意味着字典的内容并未按值复制,而只是创建了一个新的引用。

>>> a = {1: [1,2,3]}
>>> b = a.copy()
>>> a, b
({1: [1, 2, 3]}, {1: [1, 2, 3]})
>>> a[1].append(4)
>>> a, b
({1: [1, 2, 3, 4]}, {1: [1, 2, 3, 4]})

相反,深拷贝将通过值复制所有内容。

>>> import copy
>>> c = copy.deepcopy(a)
>>> a, c
({1: [1, 2, 3, 4]}, {1: [1, 2, 3, 4]})
>>> a[1].append(5)
>>> a, c
({1: [1, 2, 3, 4, 5]}, {1: [1, 2, 3, 4]})

所以:

  1. b = a: 引用赋值,使 ab 指向同一个对象。

    Illustration of 'a = b': 'a' and 'b' both point to '{1: L}', 'L' points to '[1, 2, 3]'.

  2. b = a.copy(): 浅拷贝,ab 成为两个独立的对象,但它们的内容仍共享相同的引用。

    Illustration of 'b = a.copy()': 'a' points to '{1: L}', 'b' points to '{1: M}', 'L' and 'M' both point to '[1, 2, 3]'.

  3. b = copy.deepcopy(a): 深拷贝,ab 的结构和内容都变得完全隔离。

    Illustration of 'b = copy.deepcopy(a)': 'a' points to '{1: L}', 'L' points to '[1, 2, 3]'; 'b' points to '{1: M}', 'M' points to a different instance of '[1, 2, 3]'.


1
很好的答案,但您可以考虑纠正第一句中的语法错误。在b中再次使用L没有任何问题。这样做会简化示例。 - Tom Russell
1
@JavaSa 在第一个例子中,a[1] = {} 会影响到 b,而在第二个例子中则不会。 - kennytm
@TomRussell 只是为了澄清,“第一个例子”指的是图片中的第一个例子(即b = a.copy()而不是b = a)。 - kennytm
4
非常棒的解释,真的帮我解决了问题!谢谢...这个方法能否同样适用于 Python 中的列表、字符串和其他数据类型? - Bhuro
1
@Sheldore 当你执行 dic_b = dic_a 时,dic_adic_b 引用的是同一个字典。虽然字典中的字符串是不可变的,但这并不重要,因为 dic_b['A'] = 'Adam' 实际上是在改变字典本身。 - kennytm
显示剩余10条评论

51

请看这个例子:

original = dict(a=1, b=2, c=dict(d=4, e=5))
new = original.copy()

现在,让我们在“浅层”(第一层)中更改一个值:

new['a'] = 10
# new = {'a': 10, 'b': 2, 'c': {'d': 4, 'e': 5}}
# original = {'a': 1, 'b': 2, 'c': {'d': 4, 'e': 5}}
# no change in original, since ['a'] is an immutable integer

现在让我们更改一个层级更深的值:

new['c']['d'] = 40
# new = {'a': 10, 'b': 2, 'c': {'d': 40, 'e': 5}}
# original = {'a': 1, 'b': 2, 'c': {'d': 40, 'e': 5}}
# new['c'] points to the same original['d'] mutable dictionary, so it will be changed

19
“no change in original, since ['a'] is an immutable integer”这句话的意思是,“原始内容没有改变,因为['a']是一个不可变的整数”。该句回答了所提出的问题。 - CivFan
整数不可变的事实并不是原始数据保持不变的原因。当你使用可变对象而不是1和10作为值时,你可以观察到相同的行为。 - julaine

42

这不是深拷贝还是浅拷贝的问题,你所做的都不是深拷贝。

这里:

>>> new = original 

你正在创建一个新的引用,指向原始引用的列表/字典。

而在这里:

>>> new = original.copy()
>>> # or
>>> new = list(original) # dict(original)

你正在创建一个新的列表/字典,其中填充有原始容器中包含对象的引用的副本。


确实,就目前而言是正确的...但它并不能回答关于浅层和深层的OP问题。 - MarkHu

12

补充kennytm的答案。当你进行浅拷贝 parent.copy() 时,会创建一个新的字典,具有相同的键,但值没有被复制,它们被引用。如果您向 parent_copy 添加新值,则不会影响 parent,因为parent_copy是一个新字典,而不是参考。

parent = {1: [1,2,3]}
parent_copy = parent.copy()
parent_reference = parent

print id(parent),id(parent_copy),id(parent_reference)
#140690938288400 140690938290536 140690938288400

print id(parent[1]),id(parent_copy[1]),id(parent_reference[1])
#140690938137128 140690938137128 140690938137128

parent_copy[1].append(4)
parent_copy[2] = ['new']

print parent, parent_copy, parent_reference
#{1: [1, 2, 3, 4]} {1: [1, 2, 3, 4], 2: ['new']} {1: [1, 2, 3, 4]}
parent[1]parent_copy[1]的哈希值相同,这意味着parent[1]parent_copy[1]中的[1,2,3]存储在id为140690938288400的位置。
但是parentparent_copy的哈希值不同,这意味着它们是不同的字典,parent_copy是一个新的字典,其中的值引用了parent的值。

5
"new"和"original"是不同的字典,这就是为什么你只能更新其中一个的原因。 "items"只进行浅复制,而不是整个字典本身。

3
在你的第二部分中,你应该使用new = original.copy() .copy= 是不同的东西。

2

Contents 被浅复制。

如果原始的 dict 包含一个 list 或另一个 dictionary,在原始或其浅层拷贝中修改其中一个将会同时修改另一个(listdict)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接