请选择 进入手机版 | 继续访问电脑版
设为首页收藏本站

Crossin的编程教室

 找回密码
 立即加入
查看: 15114|回复: 0

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

[复制链接]

169

主题

1

好友

733

积分

版主

Rank: 7Rank: 7Rank: 7

发表于 2018-9-14 21:02:15 |显示全部楼层

之前关于 Python 的作用域、赋值、参数传递,我们接连谈了几篇文章:
全菊变量和菊部变量关于函数参数传递,80%人都错了可变对象与不可变对象
今天我们依然要就相关话题继续下去。

首先是上次最后的思考题:
  1. m = [1, 2, [3]]
  2. n = m[:]
  3. n[1] = 4
  4. n[2][0] = 5
  5. print(m)
复制代码
m 的结果是什么?

正确答案是 [1, 2, [5]],这次比上次好点,有 35% 的正确率。

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

假设有这样一个 list 变量 m,其中有 4 个元素(别被嵌套迷惑了):
  1. m = [1, 2, [3, 4], [5, [6, 7]]]
复制代码
为了更直观的表示,我来画个图:

现在我们想要再来“复制”一个同样的变量。也许第一个闪过脑中的念头就是:
  1. n = m
复制代码
但看了前面的文章后你应该知道,这样的赋值只相当于增加了一个标签,并没有新的对象产生:

用 id 验证下就知道,m 和 n 仍然是同一个东西。那么他们内部的元素自然也是一样的,对其中一个进行修改,另一个也会跟着变:
  1. m = [1, 2, [3, 4], [5, [6, 7]]]
  2. print('m:', id(m))
  3. print([id(i) for i in m])
  4. n = m
  5. print('n:', id(n))
  6. print([id(i) for i in n])
  7. print(n is m)
  8. print(n[0] is m[0])
  9. print(n[2] is m[2])
  10. n[0] = -1
  11. print(m)
  12. n[2][1] = -1
  13. print(m)
复制代码
输出
  1. m: 4564554888
  2. [4556507504, 4556507536, 4564554760, 4564555016]
  3. n: 4564554888
  4. [4556507504, 4556507536, 4564554760, 4564555016]
  5. True
  6. True
  7. True
  8. [-1, 2, [3, 4], [5, [6, 7]]]
  9. [-1, 2, [3, -1], [5, [6, 7]]]
复制代码
因此有人将此操作称为“旧瓶装旧酒”,只是多贴了一层标签,这不能达到我们的目的。要得到一个对象的“拷贝”,我们需要用到 copy 方法:
  1. from copy import copy
  2. m = [1, 2, [3, 4], [5, [6, 7]]]
  3. print('m:', id(m))
  4. print([id(i) for i in m])
  5. n = copy(m)
  6. print('n:', id(n))
  7. print([id(i) for i in n])
  8. print(n is m)
  9. print(n[0] is m[0])
  10. print(n[2] is m[2])
  11. n[0] = -1
  12. print(m)
  13. n[2][1] = -1
  14. print(m)
复制代码
输出
  1. m: 4340253832
  2. [4333009264, 4333009296, 4340253704, 4340253960]
  3. n: 4340268104
  4. [4333009264, 4333009296, 4340253704, 4340253960]
  5. False
  6. True
  7. True
  8. [1, 2, [3, 4], [5, [6, 7]]]
  9. [1, 2, [3, -1], [5, [6, 7]]]
复制代码
从结果中可以看出,n 和 m 已不是同一个对象,对于某个元素的重新赋值不会影响原对象。但是,它们内部的元素全都是一样的,所以对一个可变类型元素的修改,则仍然会反应在原对象中。

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

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

如果要彻底地产生一个和原对象完全独立的复制品,得使用深拷贝(deep copy):
  1. from copy import deepcopy
  2. m = [1, 2, [3, 4], [5, [6, 7]]]
  3. print('m:', id(m))
  4. print([id(i) for i in m])
  5. n = deepcopy(m)
  6. print('n:', id(n))
  7. print([id(i) for i in n])
  8. print(n is m)
  9. print(n[0] is m[0])
  10. print(n[2] is m[2])
  11. n[0] = -1
  12. print(m)
  13. n[2][1] = -1
  14. print(m)
复制代码
输出
  1. m: 4389131400
  2. [4381886832, 4381886864, 4389131272, 4389131528]
  3. n: 4389131208
  4. [4381886832, 4381886864, 4389131656, 4389145736]
  5. False
  6. True
  7. False
  8. [1, 2, [3, 4], [5, [6, 7]]]
  9. [1, 2, [3, 4], [5, [6, 7]]]
复制代码
此时,对新对象中元素做任何改动都不会影响原对象。新对象中的子列表,无论有多少层,都是新的对象,有不同的地址。

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

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

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

不过额外要说提醒一下的是,深拷贝的实现过程并不是完全的递归,否则如果对象的某级子元素是它自身的话,这个过程就死循环了。实际上,如果遇到已经处理过的对象,就会直接使用其引用,而不再重复处理。听上去有点难懂是不是?想想这个例子大概就会理解了:
  1. from copy import deepcopy
  2. m = [1, 2]
  3. m.append(m)
  4. print(m, id(m), id(m[2]))
  5. n = deepcopy(m)
  6. print(n, id(n), id(n[2]))
复制代码
输出
  1. [1, 2, [...]] 4479589576 4479589576
  2. [1, 2, [...]] 4479575048 4479575048
复制代码
最后,还是给各位留个思考:
  1. from copy import deepcopy
  2. a = [3, 4]
  3. m = [1, 2, a, [5, a]]
  4. n = deepcopy(m)
  5. n[3][1][0] = -1
  6. print(n)
复制代码
深拷贝后的 n,修改了其中一个元素值,会是怎样的效果?

思考一下输出会是什么?

然后自己在电脑上或者我们的在线编辑器 Crossin的编程教室 - 在线Python编辑器 里输入代码运行下看看结果,再想想为什么。

欢迎留言给出你的解释。



════

其他文章及回答:

如何自学Python | 新手引导 | 精选Python问答 | Python单词表 | 人工智能 | 嘻哈 | 爬虫 | 我用Python | 高考 | requests | AI平台

欢迎搜索及关注:Crossin的编程教室









回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即加入

QQ|手机版|Archiver|Crossin的编程教室 ( 苏ICP备15063769号  

GMT+8, 2024-3-29 13:26 , Processed in 0.023812 second(s), 23 queries .

Powered by Discuz! X2.5

© 2001-2012 Comsenz Inc.

回顶部