首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >开源图书《Python完全自学教程》5.3引用和拷贝

开源图书《Python完全自学教程》5.3引用和拷贝

作者头像
老齐
发布2022-05-17 09:42:06
发布2022-05-17 09:42:06
28200
代码可运行
举报
文章被收录于专栏:老齐教室老齐教室
运行总次数:0
代码可运行

5.3 引用和拷贝

在第2章2.3节学习变量的时候曾强调过 Python 中的变量与对象之间是引用关系。以列表为例:

代码语言:javascript
代码运行次数:0
运行
复制
>>> lst1 = [1, 2, 3]
>>> lst2 = lst1
>>> id(lst1)
140425588751424
>>> id(lst2)
140425588751424

变量 lst1lst2 引用了同一个对象,如果借用 lst1 修改该对象成员,会发现 lst1lst2 “同步变化”——本质上是同一个列表对象内的成员变化。

代码语言:javascript
代码运行次数:0
运行
复制
>>> lst1.append(4)
>>> lst1
[1, 2, 3, 4]
>>> lst2
[1, 2, 3, 4]

同样的情况也适用于字典和集合。

代码语言:javascript
代码运行次数:0
运行
复制
# 变量引用字典
>>> dct1 = {'lang':'python'}
>>> dct2 = dct1
>>> dct2 is dct1
True
>>> dct1['name'] = 'laoqi'
>>> dct1
{'lang': 'python', 'name': 'laoqi'}
>>> dct2
{'lang': 'python', 'name': 'laoqi'}

# 变量引用集合
>>> s1 = set('book')
>>> s2 = s1
>>> s1 is s2
True
>>> s1.add('hello')
>>> s1
{'o', 'b', 'k', 'hello'}
>>> s2
{'o', 'b', 'k', 'hello'}

复习了变量与对象之间的“引用”关系之后,再来探讨列表、字典、集合都有的一个方法:copy() ,先观察它们的帮助文档是如何描述这个方法的:

  • 列表:Return a shallow copy of the list
  • 字典:D.copy() -> a shallow copy of D
  • 集合:Return a shallow copy of a set

在帮助文档中都用到了“ shallow copy ”这个词语,中文翻译为“浅拷贝”,所谓“浅”是如何体现的?以列表为例:

代码语言:javascript
代码运行次数:0
运行
复制
>>> lst1 = [1, 2, 3]
>>> lst2 = lst1.copy()    # (1)
>>> lst2 is lst1          # (2)
False
>>> lst2 == lst1          # (3)
True

注释(1)执行了列表的 copy() 方法,得到了变量 lst2 引用的一个新对象,注释(2)的结果显示 lst1lst2 分别引用了两个不同的列表,但是它们的内容完全一样,所以注释(3)的结果为 True 。这符合我们通常理解的“copy”含义:复制一次之后有了两个完全一样的对象。此时,如果修改 lst1 引用的对象,lst2 引用的对象则不会随之改变。

代码语言:javascript
代码运行次数:0
运行
复制
>>> lst1.append(4)
>>> lst1
[1, 2, 3, 4]
>>> lst2
[1, 2, 3]

通过上面操作,我们已经明确,运用其 copy() 方法,得到了一个新的对象。然而,再向下考察:两个不同容器里的“东西”是否不同?——直觉上,应该是不同的对象,即 lst1 中的 1lst2 中的 1 不是同一个对象。

代码语言:javascript
代码运行次数:0
运行
复制
>>> lst1[0] is lst2[0]
True
>>> lst1[1] is lst2[1]
True
>>> lst1[2] is lst2[2]
True

仅以每个列表中的前三个为例,所发现的结果是不是有点“反直觉”:当注释(1)执行完之后,两个不同的容器里面居然“装着”同一个对象。如果用更严谨但稍显啰嗦的语言表述:执行了注释(1)的 copy() 方法之后,得到的用变量 lst2 引用的列表与 lst1 引用的列表不是同一个对象,但两个列表中的成员,是同一个对象(如图5-3-1所示)。

图5-3-1 列表浅拷贝后对象关系

copy() 方法的这个效果最大好处是节省了内存空间,一个对象被两个不同“筐”里面的“位置”引用——现实生活中是无法做到同一个苹果既在这个筐也在那个筐里的。

代码语言:javascript
代码运行次数:0
运行
复制
>>> lst1[0] = 9    # (4)
>>> lst1
[9, 2, 3]
>>> lst2
[1, 2, 3]

注释(4)令列表 list1 的第一个位置引用对象 9 ,其他不变,如图5-3-2所示。

图5-3-2 更改列表中成员

由此可见,copy() 方法“一点也不浪费”。固然“节约光荣”,但是,不小心也会容易遇到 Bug ,例如:

代码语言:javascript
代码运行次数:0
运行
复制
>>> lst3 = [1, 2, [3, 4]]
>>> lst4 = lst1.copy()
>>> lst3 is lst4
False

变量 lst3 引用的列表与 lst1 的不同之处在于列表里面有一个成员还是列表,即“容器套容器”。再执行 copy() 后得到 lst4 引用的另外一个对象。

代码语言:javascript
代码运行次数:0
运行
复制
>>> lst3[2].append(9)    # (5)
>>> lst3
[1, 2, [3, 4, 9]]

注释(5)旨在对列表 lst3 中的索引是 2 的列表成员追加一个对象。请思考,执行此操作之后,lst4 引用的列表是否还是 [1, 2, [3, 4]]

继续延续前面的思想——列表中的成员位置引用了对象。如图5-3-3所示,列表 lst3lst4 的索引 2 的位置都引用了同一个列表对象。

图5-3-3 列表中含列表

当执行注释(5)之后,向该列表对象 [3, 4] 中追加了一个整数 9 ,且此列表对象原地修改,即 lst3lst4 的索引 2 的位置所引用的对象没有变——变的是它里面的成员。因此,列表 lst4 不再是 [1, 2, [3, 4]] ,而是:

代码语言:javascript
代码运行次数:0
运行
复制
>>> lst4
[1, 2, [3, 4, 9]]

与此类似,字典、集合中的成员与相应的对象之间都是“引用”关系,在执行 cop() 方法时也会看到类似以上列表的现象,例如:

代码语言:javascript
代码运行次数:0
运行
复制
>>> d1 = {'name':"laoqi", 'city':['shanghai', 'soochow']}
>>> d2 = d1.copy()
>>> d1 is d2
False
>>> d2
{'name': 'laoqi', 'city': ['shanghai', 'soochow']}
>>> d1['name'] = '老齐'
>>> d1
{'name': '老齐', 'city': ['shanghai', 'soochow']}
>>> d2
{'name': 'laoqi', 'city': ['shanghai', 'soochow']}
>>> d1['city'].append('hangzhou')
>>> d1
{'name': '老齐', 'city': ['shanghai', 'soochow', 'hangzhou']}
>>> d2
{'name': 'laoqi', 'city': ['shanghai', 'soochow', 'hangzhou']}

运用归纳法,可以将 copy() 方法理解为:

  • 新建一个容器对象;
  • 新的容器对象相对于原来的容器对象,只复制了“最外层”容器对象;
  • 新容器内的成员与旧容器内的相应的成员,都引用同一个对象。

这就是帮助文档中的“shallow copy”——“浅拷贝”之含义。

下面用 for 循环语句(参阅第6章6.3节)将列表、字典、集合三个容器“浅拷贝”前后的成员引用对象的内存地址打印出来,从中进一步理解上述“浅拷贝”的含义。

代码语言:javascript
代码运行次数:0
运行
复制
# 列表
>>> lst1 = [1, 2, [3, 4]]
>>> lst2 = lst1.copy()
>>> id1 = {id(e):e for e in lst1}
>>> id2 = {id(e):e for e in lst2}
>>> print(f'{id(lst1)}:lst1 => {id1}')
140425643229312:lst1 => {140425563347248: 1, 
                         140425563347280: 2, 
                         140425643297088: [3, 4]}
>>> print(f'{id(lst2)}:lst2 => {id2}')
140425643230080:lst2 => {140425563347248: 1, 
                         140425563347280: 2, 
                         140425643297088: [3, 4]}
  
# 字典
>>> d1 = {'name':"laoqi", "city":["shanghai", "soochow"]}
>>> d2 = d1.copy()
>>> id1 = {id(v):v for _,v in d1.items()}
>>> id2 = {id(v):v for _,v in d2.items()}
>>> print(f"{id(d1)}:d1 ==> {id1}")
140425643232704:d1 ==> {140425643295152: 'laoqi', 
                        140425588845120: ['shanghai', 'soochow']}
>>> print(f"{id(d2)}:d2 ==> {id2}")
140425643283776:d2 ==> {140425643295152: 'laoqi', 
                        140425588845120: ['shanghai', 'soochow']}
  
# 集合
>>> s1 = {1, 2}
>>> s2 = s1.copy()
>>> id1 = {id(e):e for e in s1}
>>> id2 = {id(e):e for e in s2}
>>> print(f"{id(s1)}:s1 ==> {id1}")
140425588735776:s1 ==> {140425563347248: 1, 
                        140425563347280: 2}
>>> print(f"{id(s2)}:s2 ==> {id2}")
140425588770176:s2 ==> {140425563347248: 1, 
                        140425563347280: 2}

“浅拷贝”仅仅拷贝了容器的“最外层”,如果想得到容器内所有成员的“拷贝”,copy() 方法就无能为力了,必须使用另外一个专门工具。

代码语言:javascript
代码运行次数:0
运行
复制
>>> import copy                  # (6)
>>> lst1
[1, 2, [3, 4]]
>>> lst3 = copy.deepcopy(lst1)    # (7)
>>> lst1 is lst3
False

注释(6)引入了标准库中的模块 copy ,注释(7)使用 copy.deepcopy() 函数得到了 lst1 的“一份拷贝” lst3 ,它与 lst1 不是同一个对象,但这还不能说明它是否连同容器内部的对象都复制了一份。

代码语言:javascript
代码运行次数:0
运行
复制
>>> {id(e):e for e in lst1}
{140425563347248: 1, 140425563347280: 2, 140425643297088: [3, 4]}
>>> {id(e):e for e in lst3}
{140425563347248: 1, 140425563347280: 2, 140425643296384: [3, 4]}

比较以上所显示的每个成员对象的内存地址,会发现,注释(7)中的 copy.deepcopy() 不仅仅“拷贝了最外层容器”,也“拷贝了内层的容器”,这就是“深拷贝”。

建议读者将注释(7)的深拷贝操作也应用于字典,并检查内存地址,从而深刻理解深拷贝和浅拷贝的不同。

自学建议 到现在为止,已经学完了 Python 内置对象,它们是以后编写程序的基础,务必要熟练掌握。为此,提出以下建议供读者参考:

  • 根据本书内容,对一种对象类型进行总结,最好能绘制思维导图。
  • 阅读每种对象的普通方法的官方文档或帮助文档,从而加深对其调用方式的理解。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 老齐教室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 5.3 引用和拷贝
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档