Python进阶:浅拷贝与深拷贝

Python进阶:浅拷贝与深拷贝

HackPython致力于有趣有价值的编程教学

简介

Python中的拷贝分为浅拷贝与深拷贝,如果学习过C语言的指针,会发觉其中的一些共性。如果你不了解浅拷贝与深拷贝,那么在使用Python的过程中,就容易出现意料之外的状态?。

变量内存地址

在Python中,可以使用id()方法来查看变量所在的内存地址?,如果变量的内存地址相同,通常变量这个盒子存放的值是相同的。

通常为了判断变量的值是否相同,会使用’‘来判断,’‘其实调用了变量的__eq__()方法,该方法是可以被我们复写的?,比如想通过’=='来判断我们自己定义的结构,就可以重写其__eq__(),在Python中,方法名称前后有双下滑线的都可以称为魔术方法,在后面会有专门的章节来讨论Python中的魔术方法?。

而’is’操作符用于比较对象的内存地址,如果两个对象完全相同,它们就会具有同一个内存地址?,可以看到下面的代码

In [1]: a = 1

In [2]: b = 1

In [3]: a == b
Out[3]: True

In [4]: a is b
Out[4]: True

In [5]: id(a)
Out[5]: 4339733936

In [6]: id(b)
Out[6]: 4339733936

In [7]: a = 257

In [8]: b = 257

In [9]: a == b
Out[9]: True

In [10]: a is b
Out[10]: False

In [11]: id(a)
Out[11]: 4379959120

In [12]: id(b)
Out[12]: 4379958800

在上述代码中,当我们将1赋值给a与b时,发现a与b具有相同的id,此时通过is操作符判断a与b是相等的,而当前我们将257赋值给a与b时,却发现a与b具有不同的id,is操作符判断a与b不相等,而’=='判断两变量的值是相等的,这是为什么??

这是因为出于性能的考虑,Python内部会将-5到256的整型数据在一开始就缓存到内存中?,这样当我们要使用-5到256之间的数值时,Python不会创建一个内存地址来存放这些数,而是直接从缓存中获取,从而导致a变量与b变量为1时,其内存地址是相同的,而当我们赋值一个大于256或小于-5的数时,Python才会为这个新数创建一个新的内存?,上面的代码中使用了2次257,则会创建2个用于存放257的内存地址?。

在实际项目中,我们通常不关心变量的内存地址,所以’'更常被使用,但值得一提的是,‘is’操作符会比’‘的速度要更快?,这是因为使用’=='时,Python还需要查找当前程序中有没有对__eq__进行重写的逻辑。

浅拷贝与深拷贝

有了内存地址的概念后,理解浅拷贝与深拷贝就会轻松一些,先来看一下两者的定义。

?浅拷贝:Python会分配一块新的内存用于创建新的拷贝对象,但拷贝对象中的元素依旧是原对象(被拷贝对象)中元素,即拷贝对象与原对象的内存地址不同,但两者中的元素具有相同的内存地址

?深拷贝:Python会分配一块新的内存用于创建新的拷贝对象,拷贝对象中的元素是通过递归的方式将原对象中的元素一一复制过来的(不可变元素除外),即对象与对象中的元素都是不同的内存地址,两者完全独立分离

为了理解上面的定义,看一下下面的简短代码:

In [14]: l1 = [[1,2], (3,4)]

In [15]: l2 = l1

In [16]: id(l2)
Out[16]: 4380167560

In [17]: id(l1)
Out[17]: 4380167560

In [18]: l3 = list(l1)

In [19]: id(l3)
Out[19]: 4379166664

In [20]: l1[0].append(3)

In [21]: l1
Out[21]: [[1, 2, 3], (3, 4)]

In [22]: l2
Out[22]: [[1, 2, 3], (3, 4)]

In [23]: l3
Out[23]: [[1, 2, 3], (3, 4)]

In [24]: l1[1] += (5,6)

In [25]: l1
Out[25]: [[1, 2, 3], (3, 4, 5, 6)]

In [26]: l2
Out[26]: [[1, 2, 3], (3, 4, 5, 6)]

In [27]: l3
Out[27]: [[1, 2, 3], (3, 4)]

在上述代码中,创建了l1,l2,l3这3个变量,其中l2通过’=‘操作符创建,此时l2与l1是完全相同的,即两个变量都指向了相同的内存地址?,而l3是l1通过list()方法创建的,其实l3就是l1的浅拷贝?,对于像列表这样的可变序列,可以通过list()方法实现浅拷贝,还可以通过切片操作’:'实现浅拷贝,此外还可以利用copy.copy()方法实现浅拷贝,copy.copy()方法可以创建任意数据类型的浅拷贝?。

In [31]: l4 = copy.copy(l1)

In [32]: id(l4)
Out[32]: 4380185544

创建了浅拷贝后,可以发现l3的内存地址与l1是不同的,而l2与l1是相同的,无论我们怎么修改l1,l2都会发生相同的改变,可以将l2理解为l1的别名,两者其实是相同的东西?,而l3有所不同,当我们通过append()方法向l1的第一个元素,即[1,2]这个列表中添加新的元素时,l3也受到了影响,而当我们改动l1的第二个元素时,l3却没有改变,这是为何??

回忆一下浅拷贝的定义,浅拷贝会创建一个新的内存空间用于存储拷贝变量,但拷贝变量中的元素与原变量是相同的,来看一下l1与l3变量中的元素其内存地址。

In [28]: id(l1[0])
Out[28]: 4380900232

In [29]: id(l3[0])
Out[29]: 4380900232

可以发现两者是相同的,那为何修改l1的第二个元素时,l3的第二元素没有发生改变呢??这是因为l1的第二个元素是元组tuple,而元组是不可变的,当进行l1[1] += (5,6)操作时,其实创建了一个新的元组,此时内存地址已经改变了?,而l3中第二个元素依旧是旧元组,此时两者已经不同了。

从上面的讨论可知,如果原对象中的元素本身是不可变的,那么使用浅拷贝没有所谓,但如果原对象中存在可变对象,此时就要考虑到上面的情况了,避免程序出现逻辑错误?。

上面是浅拷贝时常需要注意的问题,接着来看深拷贝,使用深拷贝的话,拷贝出的对象完全与原对象相互独立互不影响,在使用深拷贝时,会对原对象中的元素进行递归拷贝(不可变元素除外)?,这样造成深拷贝相比浅拷贝会更慢一些,在Python中使用copy.deepcopy()进行深拷贝?,简单使用一下。

In [33]: import copy

In [34]: l1 = [[1,2], (3,4)]

In [35]: l2 = copy.deepcopy(l1)

In [36]: id(l1)
Out[36]: 4381008200

In [37]: id(l2)
Out[37]: 4379242184

In [38]: id(l1[1])
Out[38]: 4379821960

In [39]: id(l2[1])
Out[39]: 4379821960

In [40]: id(l1[0])
Out[40]: 4379736456

In [41]: id(l2[0])
Out[41]: 4380357960

从上面的代码可以看出,l1与l2的内存地址不同,其中的第一个元素的内存地址也不同,但第二个元素内存地址相同?,这是因为第一个元素为列表,列表是可变元素,如果内存地址相同,修改时拷贝对象与原对象就会互相产生影响(想浅拷贝那样),所以在深拷贝中会存放在不同的内存空间中?,而第二个元素是不可变元素,"改变"它的时候Python本身就会自动创建一个新的内存空间来存储"改变"后的值,所以深拷贝时,不可变元素的内存地址不改变也不会有所影响?,验证一下。

In [42]: l1.append(10)

In [43]: l1[0].append(6)

In [44]: l1
Out[44]: [[1, 2, 6], (3, 4), 10]

In [45]: l2
Out[45]: [[1, 2], (3, 4)]

可以发现l2不会有任何变动。

结尾

本节简单的讨论了Python中内存地址、浅拷贝与深拷贝相关的内容欢迎学习 HackPython 的教学课程并感觉您的阅读与支持。

??

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值