大家都是拷贝,凭什么你这么秀?

简介: 当时我留了个提示,说和浅拷贝、深拷贝有关,现在我们就来具体说一说。

之前关于 Python 的作用域、赋值、参数传递,我们接连谈了几篇文章:



今天我们依然要就相关话题继续下去。


首先是上次最后的思考题:


m = [1, 2, [3]]
n = m[:]
n[1] = 4
n[2][0] = 5
print(m)


m 的结果是什么?



这次比上次好点,有 35% 的正确率。


当时我留了个提示,说和浅拷贝、深拷贝有关,现在我们就来具体说一说。


假设有这样一个 list 变量 m,其中有 4 个元素(别被嵌套迷惑了):


m = [1, 2, [3, 4], [5, [6, 7]]]


为了更直观的表示,我来画个图:



现在我们想要再来“复制”一个同样的变量。也许第一个闪过脑中的念头就是:


n = m


但看了前面的文章后你应该知道,这样的赋值只相当于增加了一个标签,并没有新的对象产生



id 验证下就知道,m 和 n 仍然是同一个东西。那么他们内部的元素自然也是一样的,对其中一个进行修改,另一个也会跟着变:


m = [1, 2, [3, 4], [5, [6, 7]]]
print('m:', id(m))
print([id(i) for i in m])
n = m
print('n:', id(n))
print([id(i) for i in n])
print(n is m)
print(n[0] is m[0])
print(n[2] is m[2])
n[0] = -1
print(m)
n[2][1] = -1
print(m)


输出


m: 4564554888
[4556507504, 4556507536, 4564554760, 4564555016]
n: 4564554888
[4556507504, 4556507536, 4564554760, 4564555016]
True
True
True
[-1, 2, [3, 4], [5, [6, 7]]]
[-1, 2, [3, -1], [5, [6, 7]]]


因此有人将此操作称为“旧瓶装旧酒”,只是多贴了一层标签,这不能达到我们的目的。要得到一个对象的“拷贝”,我们需要用到 copy 方法:


from copy import copy
m = [1, 2, [3, 4], [5, [6, 7]]]
print('m:', id(m))
print([id(i) for i in m])
n = copy(m)
print('n:', id(n))
print([id(i) for i in n])
print(n is m)
print(n[0] is m[0])
print(n[2] is m[2])
n[0] = -1
print(m)
n[2][1] = -1
print(m)


输出


m: 4340253832
[4333009264, 4333009296, 4340253704, 4340253960]
n: 4340268104
[4333009264, 4333009296, 4340253704, 4340253960]
False
True
True
[1, 2, [3, 4], [5, [6, 7]]]
[1, 2, [3, -1], [5, [6, 7]]]


从结果中可以看出,n 和 m 已不是同一个对象,对于某个元素的重新赋值不会影响原对象。但是,它们内部的元素全都是一样的,所以对一个可变类型元素的修改,则仍然会反应在原对象中。



(其实这里1、2也是指向同一个对象,但作为不可变对象来说,它们互不影响,直观上的感受就相当于是复制了一份,故简化如图上所示)


这种复制方法叫做浅拷贝shallow copy),又被人形象地称作“新瓶装旧酒”,虽然产生了新对象,但里面的内容还是来自同一份。


如果要彻底地产生一个和原对象完全独立的复制品,得使用深拷贝deep copy):


from copy import deepcopy
m = [1, 2, [3, 4], [5, [6, 7]]]
print('m:', id(m))
print([id(i) for i in m])
n = deepcopy(m)
print('n:', id(n))
print([id(i) for i in n])
print(n is m)
print(n[0] is m[0])
print(n[2] is m[2])
n[0] = -1
print(m)
n[2][1] = -1
print(m)


输出


m: 4389131400
[4381886832, 4381886864, 4389131272, 4389131528]
n: 4389131208
[4381886832, 4381886864, 4389131656, 4389145736]
False
True
False
[1, 2, [3, 4], [5, [6, 7]]]
[1, 2, [3, 4], [5, [6, 7]]]


此时,对新对象中元素做任何改动都不会影响原对象。新对象中的子列表,无论有多少层,都是新的对象,有不同的地址。



按照前面的比喻,深拷贝就是“新瓶装新酒”。


你可能会注意到一个细节:n 中的前两个元素的地址仍然和 m 中一样。这是由于它们是不可变对象,不存在被修改的可能,所以拷贝和赋值是一样的


于是,深拷贝也可以理解为,不仅是对象自身的拷贝,而且对于对象中的每一个子元素,也都进行同样的拷贝操作。这是一种递归的思想。


不过额外要说提醒一下的是,深拷贝的实现过程并不是完全的递归,否则如果对象的某级子元素是它自身的话,这个过程就死循环了。实际上,如果遇到已经处理过的对象,就会直接使用其引用,而不再重复处理。听上去有点难懂是不是?想想这个例子大概就会理解了:


from copy import deepcopy
m = [1, 2]
m.append(m)
print(m, id(m), id(m[2]))
n = deepcopy(m)
print(n, id(n), id(n[2]))


输出


[1, 2, [...]] 4479589576 4479589576
[1, 2, [...]] 4479575048 4479575048


最后,还是给各位留个思考:


from copy import deepcopy
a = [3, 4]
m = [1, 2, a, [5, a]]
n = deepcopy(m)
n[3][1][0] = -1
print(n)


上周五的送书送券活动还在进行中,详情可以翻下历史文章。我想小声地说句:可以再看看本次的挑选规则,从目前的情况来看,要拿书还是挺容易的

另外,好未来组织了一个AI训练营活动,免费但名额有限,需申请后筛选。作为合作渠道之一,如果我们这里申请人数多的话,我可以争取几个直通名额。欢迎各位点击今天的下一篇文章申请及留言。如有直通机会,我将会从留言中选取(优先考虑参与过码上行动的学员)。


如需了解视频课程及答疑群等更多服务,请号内回复 码上行动

代码相关问题可以在论坛上发帖提问 bbs.crossincode.com

推荐阅读:

开发工具 | 世界杯 | 高考 | 我用Python | 知乎 | 排序 | 朋友圈 | 电影票 | 技术宅 | 火车票 | 单词表 | 押韵工具 | 新手建议 | 就业

相关文章
|
缓存 运维 程序员
程序员进国企就必然废了吗?
程序员进国企就必然废了吗?
124 0
|
7月前
|
监控 Linux Windows
装备神器edex-ui别人以为我很屌其实我只会if和else搬运代码
装备神器edex-ui别人以为我很屌其实我只会if和else搬运代码
79 1
|
C++
【C++从0到王者】第十一站:引用计数与写时拷贝
【C++从0到王者】第十一站:引用计数与写时拷贝
52 0
|
开发工具 Python
为了追学姐,用python把她的照片做成了拼图游戏,她看了...
为了追学姐,用python把她的照片做成了拼图游戏,她看了...
154 0
|
安全 Java 数据库连接
麻了,代码改成多线程,竟有9大问题 下
麻了,代码改成多线程,竟有9大问题 下
|
消息中间件 JavaScript 小程序
麻了,代码改成多线程,竟有9大问题 上
麻了,代码改成多线程,竟有9大问题 上