与很多编程语言一样,Python的拷贝方式分为赋值、浅拷贝、深拷贝。
在学习过程中,我对拷贝的了解很模糊。在经过一系列的实验后,我对这三者的概念有了进一步的理解。
1. 赋值
首先,我们要对赋值操作有以下认识:
- 赋值是把一个对象的内存地址赋给一个变量,让变量指向该地址( 旧瓶装旧酒。旧瓶指变量, 旧酒指对象的内存地址,旧是指变量和数据产生后就保留在内存中)。
- 修改不可变类型(str、tuple)需要开辟新的内存地址空间
- 修改可变类型(list等)不需要开辟新的内存地址空间
我们通过一些例子来分析下赋值操作:
1.1 字符串赋值
- 同一str的内存地址唯一,不同str占用不同的内存地址
a = 'hello'
b = 'hello'
c = a
print(a, b, c)
# hello hello hello
[id(x) for x in [a,b,c]]
# [4526716328, 4526716328, 4526716328]
我们可以发现a, b, c三者的id是一样的,以上操作相当于c = a = b = ‘hello’,为什么呢?因为str是不可变类型,所以’hello’只有一个内存地址,a, b, c三者的id一样。赋值是系统先给一个变量或者对象(这里是’hello’)分配了内存,然后再将id赋给a, b, c,所以它们的id是相同的。
- 变量重新赋值str
a = 'world'
print(a, b, c)
# world hello hello
[id(x) for x in [a,b,c]]
# [4526651464, 4526716328, 4526716328]
这时a的id和值变了,但是b, c的id和值都未变。因为str是不可变类型,不同str占用不同的内存空间。重新赋值a需要新开辟内存空间,再把a指向新的str内存地址,所以当a的str值改变,a指向的内存地址也会改变。b, c由于字符串’hello’的不变性,所以id和值都不会发生改变。
1.2 列表赋值
- 任意一个list占用不同的内存地址,无论元素是否相同
a = ['hello']
b = ['hello']
c = a
print(a, b, c)
# ['hello'] ['hello'] ['hello']
[id(x) for x in [a, b, c]]
# [4535290824, 4535290888, 4535290824]
为何a和b的id不同?因为list是可变类型,任意一个list占用不同的内存地址。
- 修改可变类型list,不需要开辟新的内存地址
a.appand(12)
print(a, b, c)
# ['hello', 12] ['hello'] ['hello', 12]
[id(x) for x in [a,b,c]]
# [4535290824, 4535290888, 4535290824]
a和c的值均改变,id没有改变;变量b的id和值都没有改变。由于list的可变性,所以修改list的值不需要另外开辟空间,只需修改原地址的值,所以a, c的值均改变,id没有改变。
对于数字,字符串和其他原子类型对象等,没有被拷贝的说法。即便是用深拷贝,查看id也是一样的;如果对其重新赋值,也只是新创建一个对象,替换掉旧的而已。
2. 浅拷贝
所谓浅拷贝就是对引用的拷贝。
2.1 利用切片操作和工厂方法(list方法)拷贝
代码场景:有一个小伙jack,tom通过切片操作拷贝jack,anny通过工厂方法拷贝jack。
jack = ['jack', ['age', 20]]
tom = jack[:]
anny = list(jack)
print(id(jack), id(tom), id(anny))
# 4570999880 4570944008 4570999752
从id来看,三者是不同的对象。
2.2 修改原对象的值
tom[0] = 'tom'
anny[0] = 'anny'
anny[1][1] = 18
print(jack, tom, anny)
# ['jack', ['age', 18]] ['tom', ['age', 18]] ['anny', ['age', 18]]
print(id(jack), id(tom), id(anny))
# 4570999880 4570944008 4570999752
奇怪的事情发生了,jack、tom、anny的年龄都变成18了。jack、tom、anny他们应当都是不同的对象,怎么会互相影响呢?看下jack,tom,anny的内部元素每个元素id:
print([id(x) for x in jack])
# [4570490672, 4571499784]
print([id(x) for x in tom])
# [4570491232, 4571499784]
print([id(x) for x in anny])
# [4570490616, 4571499784]
恍然大悟,原来jack、tom、anny的年龄指向的是同一个list元素,修改了其中一个,当然影响其他人了。为了便于理解,我画了一个草图:
利用切片操作和工厂方法(list方法)拷贝叫浅拷贝。它只是拷贝了最外围的对象本身,内部的元素都只是拷贝了一个引用而已。
3. 深拷贝
利用copy中的deepcopy方法进行拷贝就叫做深拷贝,外围和内部元素都进行了拷贝对象本身,而不是引用。它是完全拷贝了一个副本,变量内部元素id都不一样。
3.1 copy.deepcopy
我们利用copy模块中的deepcopy方法进行深拷贝。
import copy
jack = ['jack', ['age', '20']]
tom = copy.deepcopy(jack)
anny = copy.deepcopy(jack)
print([id(x) for x in [jack, tom, anny]])
# [4526691976, 4526690440, 4526692936]。因为list是可变类型,所以jack, tom, anny的id不同
print([id(x) for x in jack])
# [4526653368, 4526690568],'jack'是str不可变类型,['age','20']是list可变类型
print([id(x) for x in tom])
# [4526653368, 4526644488]
print([id(x) for x in anny])
# [4526653368, 4526642120]
深拷贝后,可以发现jack, tom,anny的id以及元素值均不相同。这才是完全拷贝了一个副本 。
3.2 修改原数据
tom[0] = 'tom'
anny[0] = 'anny'
print(jack, tom, anny)
# ['jack', ['age', '20']] ['tom', ['age', '20']] ['anny', ['age', 20]]
anny[1][1] = 18
tom[1].append(24)
print(jack, tom, anny)
# ['jack', ['age', '20']] ['tom', ['age', '20',24]] ['anny', ['age', 18]]
修改jack, tom,anny,它们之间不会互相影响。打印出每个人的内部元素id。
print([id(x) for x in [jack, tom, anny]])
# [4526691976, 4526690440, 4526692936]
print([id(x) for x in jack])
# [139132064, 3073507244L]
print([id(x) for x in tom])
# [139137464, 139132204]
print([id(x) for x in anny])
# [139141632, 139157548]
深拷贝后,jack,tom,anny元素id均不同,内部元素也都指向了不同的对象。这是完全拷贝的一个副本,修改jack后,tom没有发生改变,因为tom是一个完全的副本,元素id与jack均不同,jack修改不影响tom。
4. 总结
赋值是把一个对象内存地址赋给一个变量,让变量指向该内存地址( 旧瓶装旧酒 )
浅拷贝是在另一块地址中创建一个新的变量或容器,但是容器内的元素的地址均是源对象的元素的地址的拷贝。也就是说新的容器中指向了旧的元素( 新瓶装旧酒 )
深拷贝是在另一块地址中创建一个新的变量或容器,同时容器内的元素的地址也是新开辟的,仅仅是值相同而已,是完全的副本。也就是说( 新瓶装新酒 )