0
点赞
收藏
分享

微信扫一扫

Python面试题目之深浅拷贝浅析

 

# copy.copy 浅拷贝 只拷贝父对象,不会拷贝对象的内部的子对象。  **只是简单的指针赋值

# copy.deepcopy 深拷贝 拷贝对象及其子对象 **指针赋值,且内容拷贝

 

用一个简单的例子说明如下:

>>>import copy
>>>a = [1, 2, 3, 4, ['a', 'b', 'c']]
>>> b = a
>>> c = copy.copy(a)
>>> d = copy.deepcopy(a)

 

很容易理解:

a是一个列表,表内元素a[4]也是一个列表(也就是一个内部子对象);

b是对a列表的又一个引用,所以a、b是完全相同的,可以通过id(a)==id(b)证明。

第4行copy.copy()是浅拷贝;

第5行copy.deepcopy()是深拷贝,通过id(c)和id(d)可以发现他们不相同,且与id(a)都不相同;

 

>>> id(a)
19276104
>>> id(b)
19276104
>>> id(c)
19113304
>>> id(d)
19286976

 

至于如何看深/浅拷贝的区别,可以通过下面的操作来展现:

>>> a.append(5)    #操作1
>>> a[4].append('hello') #操作2

 

这时再查看结果:

>>> a
[1, 2, 0, 4, ['a', 'b', 'c', 'hello'], 5]
>>> b
[1, 2, 0, 4, ['a', 'b', 'c', 'hello'], 5]
>>> c
[1, 2, 3, 4, ['a', 'b', 'c', 'hello']]
>>> d
[1, 2, 3, 4, ['a', 'b', 'c']]

 

可以发现:

a、b受了操作1、2的影响;

c只受操作2影响;

d不受影响;

 

a、b结果相同很好理解。

 

由于c是a的浅拷贝,只拷贝了父对象,因此a的子对象( ['a', 'b', 'c', 'hello'])改变时会影响到c;

d是深拷贝,完全不受a的影响;

 

浅拷贝是指拷贝的只是原对象元素的引用,换句话说,浅拷贝产生的对象本身是新的,但是它的内容不是新的,只是对原对象的一个引用。这里有个例子

>>> L1=[[1, 2], 3, 4]
>>> L2 = L1[:] #利用切片完成一次浅拷贝
>>> id(L1)
3084416588L
>>> id(L2)
3084418156L
>>> L1[0][0] = 5
>>> print(L1)
[[5, 2], 3, 4]
>>> print(L2)
[[5, 2], 3, 4]

可以看到,浅拷贝生产了一个新的对象列表L2,但是列表L2的内容确实对L1的引用,所以但改变L1中值的时候,L2的值也跟着变化了。

但是有点需要特别提醒的,如果对象本身是不可变的,那么浅拷贝时也会产生两个值,见这个例子:

>>> L1 = [1, 2]
>>> L2 = L1[:]
>>>print((L2)
[1, 2]
>>> print(L1)
[1, 2]
>>> L1[1]=111
>>> print(L1)
[1, 111]
>>> print(L2)
[1, 2]

 

为什么L2的第二个元素没有变成111呢?因为数字在python中是不可变类型!!

这个顺便回顾下Python标准类型的分类:

# 可变类型: 列表,字典
# 不可变类型:数字,字符串,元组

 

理解了浅拷贝,深拷贝是什么自然就很清楚了。
python中有一个模块copy,deepcopy函数用于深拷贝,copy函数用于浅拷贝。

最后,对象的赋值是深拷贝还是浅拷贝?
对象赋值实际上是简单的对象引用

>>> a = 1
>>> id(a)
135720760
>>> b = a
>>> id(b)
135720760

 

a和b完全是一回事。

 

详细请看下文:

 

Python中 copy, deepcopy 的区别及原因

 

虽然第一次听说 Python 中的 copydeep copy 是作为 ​​copy​​​ 模块中的两个 method。但​​它们其实是 OOP 语言中常见的概念​​。这里只说 Python,其他语言不了解。


Pythoncopy 模块中的 ​​copy()​​ method 其实是与 deep copy 相对的 shallow copy。​​copy.copy(object)​​ 就等于是对 object 做了 shallow copy


先说结论:

  • 对于简单的 object,用shallow copydeep copy没区别:

>>> import copy
>>> origin = 1
>>> cop1 = copy.copy(origin)
#cop1 是 origin 的shallow copy
>>> cop2 = copy.deepcopy(origin)
#cop2 是 origin 的 deep copy
>>> origin = 2
>>> origin
2
>>> cop1
1
>>> cop2
1
#cop1 和 cop2 都不会随着 origin 改变自己的值
>>> cop1 == cop2
True
>>> cop1 is cop2
True

  • 复杂的 object, 如 list 中套着 list 的情况,shallow copy中的子list,并未从原 object 真的「独立」出来。

也就是说,如果你改变原 object 的子 list 中的一个元素,你的 copy 就会跟着一起变。这跟我们直觉上对「复制」的理解不同。

看代码更容易理解些:

>>> import copy
>>> origin = [1, 2, [3, 4]]
#origin 里边有三个元素:1, 2,[3, 4]
>>> cop1 = copy.copy(origin)
>>> cop2 = copy.deepcopy(origin)
>>> cop1 == cop2
True
>>> cop1 is cop2
False
#cop1 和 cop2 看上去相同,但已不再是同一个object
>>> origin[2][0] = "hey!"
>>> origin
[1, 2, ['hey!', 4]]
>>> cop1
[1, 2, ['hey!', 4]]
>>> cop2
[1, 2, [3, 4]]
#把origin内的子list [3, 4] 改掉了一个元素,观察 cop1 和 cop2

 

可以看到 ​​cop1​​,也就是 shallow copy 跟着 origin 改变了。而 ​​cop2​​ ,也就是 deep copy 并没有变。

似乎 deep copy 更加符合我们对「复制」的直觉定义: 一旦复制出来了,就应该是独立的了。如果我们想要的是一个字面意义的「copy」,那就直接用 ​​deep_copy​​ 即可。

那么为什么会有 shallow copy 这样的「假」 copy 存在呢? 这就是有意思的地方了。

 

Python 与众不同的变量储存方法

Python 存储变量的方法跟其他 OOP 语言不同。它与其说是把值赋给变量,不如说是给变量建立了一个到具体值的 reference。

当在 Python 中 ​​a = something​​​ 应该理解为给 something 贴上了一个标签 a。当再赋值给 ​​a​​ 的时候,就好象把 a 这个标签从原来的 something 上拿下来,贴到其他对象上,建立新的 reference。 这就解释了一些 Python 中可能遇到的诡异情况:

>>> a = [1, 2, 3]
>>> b = a
>>> a = [4, 5, 6] //赋新的值给 a
>>> a
[4, 5, 6]
>>> b
[1, 2, 3]
# a 的值改变后,b 并没有随着 a 变

>>> a = [1, 2, 3]
>>> b = a
>>> a[0], a[1], a[2] = 4, 5, 6 //改变原来 list 中的元素
>>> a
[4, 5, 6]
>>> b
[4, 5, 6]
# a 的值改变后,b 随着 a 变了

 

上面两段代码中,​​a​​​ 的值都发生了变化。区别在于,第一段代码中是直接赋给了 ​​a​​​ 新的值(从 ​​[1, 2, 3]​​​ 变为 ​​[4, 5, 6]​​);而第二段则是把 list 中每个元素分别改变。

而对 ​​b​​​ 的影响则是不同的,一个没有让 ​​b​​ 的值发生改变,另一个变了。怎么用上边的道理来解释这个诡异的不同呢?

首次把 ​​[1, 2, 3]​​​ 看成一个物品。​​a = [1, 2, 3]​​​ 就相当于给这个物品上贴上 ​​a​​​ 这个标签。而 ​​b = a​​​ 就是给这个物品又贴上了一个 ​​b​​ 的标签。

第一种情况:

​a = [4, 5, 6]​​​ 就相当于把 ​​a​​​ 标签从 ​​[1 ,2, 3]​​​ 上撕下来,贴到了 ​​[4, 5, 6]​​ 上。

在这个过程中,​​[1, 2, 3]​​​ 这个物品并没有消失。 ​​b​​​ 自始至终都好好的贴在 ​​[1, 2, 3]​​​ 上,既然这个 reference 也没有改变过。 ​​b​​ 的值自然不变。

第二种情况:

​a[0], a[1], a[2] = 4, 5, 6​​​ 则是直接改变了 ​​[1, 2, 3]​​​ 这个物品本身。把它内部的每一部分都重新改装了一下。内部改装完毕后,​​[1, 2, 3]​​​ 本身变成了 ​​[4, 5, 6]​​。

而在此过程当中,​​a​​​ 和 ​​b​​​ 都没有动,他们还贴在那个物品上。因此自然 ​​a​​​ ​​b​​​ 的值都变成了 ​​[4, 5, 6]​​。

这部分搞明白了之后再去看 copy 的区别就容易多了。

 

言归正传,Copy时候到底发生了什么

最初对 copy 产生疑惑,是有一次想对一个复杂的 list 遍历并且做修改。

这种情况下,最好先建立一个 copy 出来:


If you need to modify the sequence you are iterating over while inside the loop (for example to duplicate selected items), it is recommended that you first make a copy. Iterating over a sequence does not implicitly make a copy.

– ​​Python Documentation​​


于是想当然用了 ​​copy.copy()​​​。结果却发现本体与 copy 之间并不是独立的。有的时候改变其中一个,另一个也会跟着改变。也就是本文一开头​​结论中提到的情况​​:

>>> import copy
>>> origin = [1, 2, [3, 4]]
#origin 里边有三个元素:1, 2,[3, 4]
>>> cop1 = copy.copy(origin)
>>> cop2 = copy.deepcopy(origin)
>>> cop1 == cop2
True
>>> cop1 is cop2
False
#cop1 和 cop2 看上去相同,但已不再是同一个object
>>> origin[2][0] = "hey!"
>>> origin
[1, 2, ['hey!', 4]]
>>> cop1
[1, 2, ['hey!', 4]]
>>> cop2
[1, 2, [3, 4]]
#把origin内的子list [3, 4] 改掉了一个元素,观察 cop1 和 cop2

 

官方解释是这样的:


The difference between shallow and deep copying is only relevant for compound objects (objects that contain other objects, like lists or class instances):



A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.



A deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original.

两种 copy 只在面对复杂对象时有区别,所谓复杂对象,是指对象中含其他对象(如复杂的 list 和 class)。

由 shallow copy 建立的新复杂对象中,每个子对象,都只是指向自己在原来本体中对应的子对象。而 deep copy 建立的复杂对象中,存储的则是本体中子对象的 copy,并且会层层如此 copy 到底。



– ​​Python Doctumentation​​


这个解释看上去略抽象。

先看这里的 shallow copy。 如图所示,cop1 就是给当时的 origin 建立了一个镜像。origin 当中的元素指向哪, cop1 中的元素就也指向哪。这就是官方 doc 中所说的 ​​inserts references into it to the objects found in the original​​ 。

 

这里的关键在于,​​origin[2]​​,也就是 [3, 4] 这个 list。根据 shallow copy 的定义,在 ​​cop1[2]​​​ 指向的是同一个 list [3, 4]。那么,如果这里我们改变了这个 list,就会导致 origin 和 cop1 同时改变。这就是为什么上边 ​​origin[2][0] = "hey!"​​​ 之后,cop1 也随之变成了 ​​[1, 2, ['hey!', 4]]​​。

再来看 deep copy。 从图中可以看出,cop2 是把 origin 每层都 copy 了一份存储起来。这时候的 ​​origin[2]​​​ 和 ​​cop2[2]​​ 虽然值都等于 [3, 4],但已经不是同一个 list了。

既然完全独立,那无论如何改变其中一个,另一个自然不会随之改变。


举报

相关推荐

0 条评论