Python 浅拷贝、深拷贝 与 赋值(=)概述:
Python 对象之间赋值时是按引用传递的
- a = 100 # a 指向了100这个值
- b = a # b 也指向了100这个值
拷贝对象,需要使用标准库中的copy模块:
- copy.copy 浅拷贝: 只拷贝父对象(拷贝顶层),不会拷贝对象的内部的子对象。
- copy.deepcopy 深拷贝: 拷贝所有对象 包括 子对象
首先来波结论:
深拷贝:
- 深拷贝是对于一个对象所有层次的拷贝(递归拷贝), 保证了数据的独立性, 一般用于备份场景
- 即将被复制的对象完全的复制一遍, 作为一个新的个体单独存在, 所以改变原有被复制对象是不会对深拷贝后的对象有影响的
等于(=)赋值:
- 赋值是将原对象的引用传递给变量, 并不会产生一个独立的对象单独存在, 它只是将原有的数据贴上一个新标签, 所以在原数据改变时, 就意味着变量指向的数据发生改变, 变量的值也就改变了
浅拷贝:
- 浅拷贝是对一个对象的 顶层进行拷贝 ( 具体分两种情况 )
- 浅拷贝对象为: 不可变对象( 数值, 字符串, 元组 )时, 此种情况和 "等于(=)赋值" 的情况一样, 原对象的id值 与 浅拷贝后的对象id值相同
- 浅拷贝对象为: 可变对象( 列表, 字典 ) 时会产生一个 "不是那么绝对独立的对象", 存在两种情况:
- 复制的 对象中无 复杂 子对象, 浅拷贝的值的改变 与 原对象的值互不影响, 原对象的 id 与 浅拷贝后的 id 也不同
- 复制的 对象中有 复杂 子对象 (例如: 列表中有一个元素是一个列表), 此时只对顶层进行拷贝, 不会拷贝子对象;
- 如果不改变 原对象中复杂子对象的值--->浅拷贝后的值并不会受原对象的值影响
- 但是只要改变原对象中 复杂子对象的值--->浅拷贝后的值会随之改变
注意:
- 如果用copy.copy(), copy.deepcopy()对一个 全部 都是不可变对象进行拷贝(例: a = [[1, 2]]), 那么它们的结果相同, 都是引用指向
- 如果拷贝的是一个拥有可变类型的数据(例: a = ([1, 2])), 即使元组是最顶层, 那么deepcopy依然是深拷贝, copy.copy还是指向引用
Python 中 copy() 与 deepcopy() 之间的主要区别是对数据的存储方式, 下面我们逐一验证一下:
- 当浅拷贝的值是不可变对象(数值,字符串,元组)时,代码如下:
>>> a = "1234567"
>>> b = a
>>> id(a)
4367619440
>>> id(b)
4367619440
>>> c = copy.copy(a)
>>> id(c)
4367619440
>>> d = copy.deepcopy(a)
>>> id(d)
4367619440
- 当浅拷贝的值是可变对象(列表,字典)时,不改变 复杂子对象 的值 代码如下:
>>> list1 = [1,2,3]
>>> list2 = list1
>>> list3 = copy.copy(list1)
>>> id(list1)
4367654664
>>> id(list2)
4367654664
>>> id(list3)
4367628616
>>> list1.append(55)
>>> print(list1)
[1, 2, 3, 55]
>>> print(list3)
[1, 2, 3]
- 当浅拷贝的值是可变对象(列表,字典)时,改变的值是 复杂子对象 代码如下:
>>> import copy
>>> list1 = [1, 2, ['a', 'b']]
>>> list2 = list1
>>> list3 = copy.copy(list2)
>>> list4 = copy.deepcopy(list3)
>>> id(list1)
4338414656
>>> id(list2)
4338414656
>>> id(list3)
4338415016
>>> id(list4)
4338414368
>>> list1[2].append('a')
>>> id(list1)
4338414656
>>> print(list1)
[1, 2, ['a', 'b', 'a']]
>>> print(list3)
[1, 2, ['a', 'b', 'a']]
>>> print(list4)
[1, 2, ['a', 'b']]
>>> list1.append(33)
>>> id(list1)
4338414656
>>> id(list3)
4338415016
>>> print(list1)
[1, 2, ['a', 'b', 'a'], 33]
>>> print(list3)
[1, 2, ['a', 'b', 'a']]
代码说明:
当改变的值是 复杂子对象中的元素时,浅拷贝值发生了变化;
当改变的值不是复杂子对象时,浅拷贝的值没有发生变化。因为 浅拷贝 ,复杂子对象的保存方式是 作为 引用 方式存储的,所以修改 浅拷贝的值 和原来的值都可以 改变 复杂子对象的值。
Python的数据存储方式:
- Python 存储变量的方法跟其他语言不同。它与其说是把值赋给变量,不如说是给变量建立了一个到具体值的引用( 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的时候到底发生了什么?
看下面代码:
>>> 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
copy(浅拷贝)对于一个复杂对象的子对象并不会完全复制,什么是复杂对象的子对象呢?就比如序列里的嵌套序列,字典里的嵌套序列等都是复杂对象的子对象。对于子对象,python会把它当作一个公共镜像存储起来,所有对他的复制都被当成一个引用,所以说当其中一个引用将镜像改变了之后另一个引用使用镜像的时候镜像已经被改变了。
所以说看这里的origin[2],也就是 [3, 4] 这个 list。根据 shallow copy 的定义,在 cop1[2] 指向的是同一个 list [3, 4]。那么,如果这里我们改变了这个 list,就会导致 origin 和 cop1 同时改变。这就是为什么上边 origin[2][0] = “hey!” 之后,cop1 也随之变成了 [1, 2, [‘hey!’, 4]]。
用 deepcopy(深拷贝)的时候会将复杂对象的每一层复制一个单独的个体出来。
这时候的 origin[2] 和 cop2[2] 虽然值都等于 [3, 4],但已经不是同一个 list了。即我们寻常意义上的复制(备份)。