【问题标题】:Reason why deep and shallow copy differences between "primitive" and "non-primitive" objects?“原始”和“非原始”对象之间的深浅复制差异的原因是什么?
【发布时间】:2020-02-14 08:33:40
【问题描述】:

我知道 Python 中浅拷贝和深拷贝的区别,问题不在于何时使用其中一种。然而,我发现这个简单的例子非常有趣且不直观

from copy import deepcopy

 a=0
 b=deepcopy(a)
 c=a
 a+=1
 print(a,b,c)

输出:1 0 0

 from copy import deepcopy
 a=[0]
 b=deepcopy(a)
 c=a
 a[0]+=1
 print(a,b,c)

输出:[1] 0 [1]

我想知道做出这种设计选择的原因,因为在我看来,这两个 sn-ps 代码非常等效,但它们的输出却完全不同。为了让自己更明确,我想知道为什么 = 在“原始”变量的情况下是深拷贝,而在“非原始”(但仍然是基本语言的一部分)变量(如列表)的情况下是浅拷贝?我个人认为这种行为违反直觉 注意:我用的是 python 3

【问题讨论】:

标签: python copy


【解决方案1】:

这里的关键是可变性不变性

在python中没有原始和非原始之分,一切都是类型,有些只是内置的。

您需要了解 python 如何将数据存储在变量中。假设您来自 C 背景,您可以认为所有 Python 变量都是指针。
所有 Python 变量都将引用存储到变量值实际所在的位置强>。

内置的id 函数让我们可以查看变量值的实际存储位置。

>>> x = 12345678
>>> id(x)
1886797010128
>>> y = x
>>> id(y)
1886797010128
>>> y += 1
>>> y
12345679
>>> x
12345678
>>> id(y)
1886794729648

变量x 指向位置1886797010128,位置1886797010128 保存10 的值。 int是python中的不可变类型,也就是说1886797010128位置存储的数据是不能改变的。

当我们分配y = x 时,y 现在也指向同一个地址,因为没有必要为同一个值分配更多的内存。

y 改变时(记住int 是一个不可变类型并且它的值不能改变),一个新的int 被创建在新的位置1886794729648y 现在指向这个新的新地址处的 int 对象。

当您尝试更新包含不可变数据的变量的值时,也会发生同样的情况。

>>> id(x)
140707671077744
>>> x = 30
>>> id(x)
140707671078064

更改具有不可变数据的变量的值只会使变量指向具有更新值的新对象。


对于像list 这样的可变类型,情况并非如此。

>>> a = [1, 2, 3]
>>> b = a
>>> id(a), id(b)
(1886794896456, 1886794896456)
>>> b.append(4)
>>> a
[1, 2, 3, 4]
>>> b
[1, 2, 3, 4]
>>>

a 是一个list 并且是可变的,使用像append 这样的方法来改变它实际上会改变地址1886794896456 的值。由于b 也指向同一个地址,a 的值也会更新。


deepcopy 在不同的内存位置创建一个与其参数值相同的新对象,即传递给它的对象。

我想知道做出这种设计选择的原因

这仅仅是因为 python 被设计为一种面向对象的语言。在 java 对象中可以看到类似的行为。

我个人认为这种行为违反直觉

直觉来自实践。练习一种语言并不能帮助了解其他语言的工作方式,不同的语言有不同的设计模式和约定,我认为应该付出一些努力来了解它们对于我们将要使用的语言是什么。

【讨论】:

  • 所以我想整数工作的原因是尽可能少地做工作,即只有在我真正需要它们时才获取新的内存资源。这是正确的推理路线吗?
  • @alessiolapolla:这与内存效率无关。当试图设计一种允许复杂数据结构而没有显式指针的语言时,“所有事物的引用”设计自然会出现,然后如果你让基本数字类型在这样的设计中是可变的,你最终不得不制作大量的显式副本在这个地方,以防止整数和浮点数意外改变。数字传递得太频繁了,以至于不切实际。
  • yy = x 之后不指向与x 相同的对象,只是为了节省内存。它指向同一个对象,因为变量赋值意味着“评估右侧的表达式并将结果引用存储在左侧的变量中”。无论可变性或不变性如何,此分配不允许创建新对象或使y 指向x 指向以外的位置。
【解决方案2】:

c = a 既不是浅拷贝也不是深拷贝,不管a 指的是什么。它甚至比这更浅 - 它只复制一个参考。在此分配之后,ca 都持有对同一对象的引用。

在 Python 中无法修改 int 的值。当您在 int 上使用 += 时,Python 会将一个(对 a 的引用)新 int 分配到您从中检索原始 int 的任何位置。

对于第一种情况,a += 1 重新分配 a 变量,而 bc 继续引用它们在赋值之前引用的整数。

对于第二种情况,a[0] += 1 重新分配 a 所指列表的单元格 0。 b 继续引用未更改的副本,c 继续引用 a 引用的相同列表。由于此列表已更改状态,因此可以通过 c 变量看到更改。


顺便说一句,deepcopy 被设计为产生一个深拷贝,从某种意义上说,对返回值的(任意深度)修改不会修改参数,反之亦然。由于无法在 Python 中修改 int 的值,因此 int 算作自身的(深)副本,实际上,deepcopy 实现只需在其参数是 int 时返回其参数。

>>> x = 1000
>>> copy.deepcopy(x) is x
True

【讨论】:

    【解决方案3】:

    在复制期间链接对象而不是基元是很常见的。

    你的sn-ps之间的区别在于,在第二个中,c是列表a的副本,而列表是一个对象,因此它们是链接的。而 c 是第一个 sn-p 中原语的“副本”,它没有链接。

    【讨论】:

    • Ints 在 Python 中也是对象。
    • 是的,你是对的,但他们保留了不能链接“原始”的约定,即使它们在技术上也是对象。
    • 这不是正在发生的事情。没有“链接”副本。相反,Python 变量保存对对象的引用,c=a 导致 ca 保存对同一对象的引用 - 它不会复制任何对象。
    • 我的意思是,python 的 int 不会引用同一个对象,以尊重您在其他编程语言(如 java)中会发现的约定。 a=0 b=a a+=1 打印(b):0
    • 这不是真的。两个 Python 变量可以很好地引用同一个 int,事实上,ca 在第一个 sn-p 中的 c=a 之后确实引用了同一个 int。他们只是在a+=1 之后停止引用同一个int,因为该分配导致a 开始引用不同的int。
    猜你喜欢
    • 2021-10-27
    • 2014-03-02
    • 1970-01-01
    • 1970-01-01
    • 2011-03-20
    • 1970-01-01
    • 2014-04-01
    • 2017-08-30
    • 2020-09-29
    相关资源
    最近更新 更多