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 中的 copy 与 deep copy 是作为 copy
模块中的两个 method。但它们其实是 OOP 语言中常见的概念。这里只说 Python,其他语言不了解。
Python 的 copy 模块中的
copy()
method 其实是与 deep copy 相对的 shallow copy。copy.copy(object)
就等于是对 object 做了 shallow copy。
先说结论:
- 对于简单的 object,用 shallow copy 和 deep 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 中的一个元素,你的 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.
于是想当然用了 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 到底。
这个解释看上去略抽象。
先看这里的 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了。
既然完全独立,那无论如何改变其中一个,另一个自然不会随之改变。