Python面试题系列之06 赋值深拷贝浅拷贝

InterviewQuestion06: 什么是赋值、深拷贝、浅拷贝?

Question

什么是赋值深拷贝浅拷贝

知识点详解

对象的赋值Assignment、拷贝Copy(深/浅拷贝)之间是有差异的,这也是面试经常遇到的问题,本文就通过一些例子来理解下它们的区别。

赋值·Assignment

所谓赋值,就是将一个对象的地址赋值给一个变量,让变量指向该地址。

>>> a = ["知识星球-ID", 91155859, ["人人", "都是", "Pythonista"]]
>>> b = a
>>> print(id(a), id(b), sep='\n')
2705125761736
2705125761736

在Python中,用一个变量给另一个变量赋值,其实就是给当前内存中的对象增加一个“标签”而已。如上面的例子,通过使用内置函数 id() ,可以看出 a 和 b 指向内存中同一个对象,此时is比较时:

>>> a is b
True

浅拷贝·ShallowCopy

所谓浅拷贝,是在另一块地址中创建一个新的变量或容器,但是容器内的元素的地址均是源对象的元素的地址的拷贝。也就是说新的容器中指向了旧的元素。(拷贝组合对象,不拷贝子对象)

常见的浅拷贝操作有以下三种方式:

  • 切片[:]操作
>>> a = ["知识星球-ID", 91155859, ["人人", "都是", "Pythonista"]]
>>> b = a[:]
>>> print(id(a), id(b))        # a 和 b 身份不同
2103264174792 2103265400456

>>> for i, j in zip(a, b):     #  从打印结果不难看出:它们包含的子对象身份相同
...     print(id(i), id(j))
... 
2103264511872 2103264511872
2103235528592 2103235528592
2103264174728 2103264174728
  • 工厂函数,如list/dir/set
>>> a = ["知识星球-ID", 91155859, ["人人", "都是", "Pythonista"]]
>>> b = list(a)
>>> print(id(a), id(b))        # a 和 b 身份不同
2455220242184 2455213751624

>>> for i, j in zip(a, b):     #  从打印结果不难看出:它们包含的子对象身份相同
...     print(id(i), id(j))
... 
2739939238872 2739939238872
2739939283632 2739939283632
2739927343624 2739927343624
  • copy模块中的copy函数
>>> a = ["知识星球-ID", 91155859, ["人人", "都是", "Pythonista"]]
>>> b = copy.copy(a)
>>> print(id(a), id(b))        # a 和 b 身份不同
2216345428872 2216345585544

>>> for i, j in zip(a, b):     #  从打印结果不难看出:它们包含的子对象身份相同
...     print(id(i), id(j))
... 
2216343799680 2216343799680
2216343259024 2216343259024
2216345430280 2216345430280

从上面的三个例子可以明显的看出来,通过 a 浅拷贝得到的 b,a 和 b 指向内存中不同的list对象,但它们的元素却指向相同的strintlist子对象。这就是浅拷贝!

深拷贝·DeepCopy

所谓“深拷贝”,是在另一块地址中创建一个新的变量或容器,同时容器内的元素的地址也是新开辟的,仅仅是值相同而已,是完全的副本。深拷贝出来的对象与原对象没有任何关联。

深拷贝只有一种方式:copy模块中的deepcopy函数。

>>> import copy
>>> a = ["知识星球-ID", 91155859, ["人人", "都是", "Pythonista"]]
>>> b = copy.deepcopy(a)
>>> print(id(a), id(b))
1538766627848 1538766784456

>>> for i, j in zip(a, b):
...     print(id(i), id(j))
... 
1538736752512 1538736752512
1538736211856 1538736211856
1538766629128 1538766784520

看了上面的例子,有人可能会疑惑:为什么使用了深拷贝,a和b中元素的id还是一样呢?

其原因就是上述拷贝操作的对象中出现了str、int这种不可变对象。对于不可变对象,当需要一个新的对象时,Python可能会返回已经存在的某个类型和值都一致的对象的引用。但这种机制并不会影响 a 和 b 的相互独立性,因为当两个元素指向同一个不可变对象时,对其中一个赋值不会影响另外一个。

我们现在用一个仅包含了可变对象的列表,来再次演示“浅拷贝”与“深拷贝”的差异:

>>> import copy
>>> a = [[1, 2],[5, 6], [8, 9]]
>>> b = copy.copy(a)            # 通过对 a 浅拷贝得到 b
>>> c = copy.deepcopy(a)        # 通过对 a 深拷贝得到 c

>>> print(id(a), id(b))         # a 和 b 不同
3095031302024 3095031301704
>>> for i, j in zip(a, b):      # 从打印结果不难看出:a和b的子对象是相同的
...     print(id(i), id(j))
... 
3095031146696 3095031146696
3095031145480 3095031145480
3095031301960 3095031301960
>>> print(id(a), id(c))         # a 和 c 不同
3095031302024 3095031301576
>>> for i, j in zip(a, c):      # 从打印结果不难看出:a和c的子对象也是不同的
...     print(id(i), id(j))
... 
3095031146696 3095031301768
3095031145480 3095031301896
3095031301960 3095031302088

通过这个例子,你应该理解了浅拷贝与深拷贝的区别了吧😁

Answer

赋值
其实就是对象的引用(别名)。可以理解为:旧瓶装旧酒。

浅拷贝
拷贝父对象,不会拷贝对象的内部的子对象。可以理解为:新瓶装旧酒。

深拷贝
完全拷贝了父对象及其子对象。可以理解为:新瓶装新酒。

对于非容器类型(如数字、字符串、和其他’原子’类型的对象)没有拷贝这一说法

后记

浅拷贝和深拷贝的不同仅仅是对组合对象来说的,所谓的组合对象就是包含了其它对象的对象,如列表、类实例等等。而对于数字、字符串以及其它“原子”类型,没有拷贝一说,产生的都是原对象的引用。

"obj is copy.copy(obj)","obj is copy.deepcopy(obj)"

好了,以上就是本篇全部内容。

备注:本篇首发于知识星球「人人都是Pythonista」。


文章作者: &娴敲棋子&
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 &娴敲棋子& !
评论
  目录