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
发出的红包

打赏作者

懒编程-二两

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值