在第2章2.3节学习变量的时候曾强调过 Python 中的变量与对象之间是引用关系。以列表为例:
>>> lst1 = [1, 2, 3]
>>> lst2 = lst1
>>> id(lst1)
140425588751424
>>> id(lst2)
140425588751424
变量 lst1
和 lst2
引用了同一个对象,如果借用 lst1
修改该对象成员,会发现 lst1
和 lst2
“同步变化”——本质上是同一个列表对象内的成员变化。
>>> lst1.append(4)
>>> lst1
[1, 2, 3, 4]
>>> lst2
[1, 2, 3, 4]
同样的情况也适用于字典和集合。
# 变量引用字典
>>> 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 ”这个词语,中文翻译为“浅拷贝”,所谓“浅”是如何体现的?以列表为例:
>>> lst1 = [1, 2, 3]
>>> lst2 = lst1.copy() # (1)
>>> lst2 is lst1 # (2)
False
>>> lst2 == lst1 # (3)
True
注释(1)执行了列表的 copy()
方法,得到了变量 lst2
引用的一个新对象,注释(2)的结果显示 lst1
和 lst2
分别引用了两个不同的列表,但是它们的内容完全一样,所以注释(3)的结果为 True
。这符合我们通常理解的“copy”含义:复制一次之后有了两个完全一样的对象。此时,如果修改 lst1
引用的对象,lst2
引用的对象则不会随之改变。
>>> lst1.append(4)
>>> lst1
[1, 2, 3, 4]
>>> lst2
[1, 2, 3]
通过上面操作,我们已经明确,运用其 copy()
方法,得到了一个新的对象。然而,再向下考察:两个不同容器里的“东西”是否不同?——直觉上,应该是不同的对象,即 lst1
中的 1
与 lst2
中的 1
不是同一个对象。
>>> 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()
方法的这个效果最大好处是节省了内存空间,一个对象被两个不同“筐”里面的“位置”引用——现实生活中是无法做到同一个苹果既在这个筐也在那个筐里的。
>>> lst1[0] = 9 # (4)
>>> lst1
[9, 2, 3]
>>> lst2
[1, 2, 3]
注释(4)令列表 list1
的第一个位置引用对象 9
,其他不变,如图5-3-2所示。
图5-3-2 更改列表中成员
由此可见,copy()
方法“一点也不浪费”。固然“节约光荣”,但是,不小心也会容易遇到 Bug ,例如:
>>> lst3 = [1, 2, [3, 4]]
>>> lst4 = lst1.copy()
>>> lst3 is lst4
False
变量 lst3
引用的列表与 lst1
的不同之处在于列表里面有一个成员还是列表,即“容器套容器”。再执行 copy()
后得到 lst4
引用的另外一个对象。
>>> lst3[2].append(9) # (5)
>>> lst3
[1, 2, [3, 4, 9]]
注释(5)旨在对列表 lst3
中的索引是 2
的列表成员追加一个对象。请思考,执行此操作之后,lst4
引用的列表是否还是 [1, 2, [3, 4]]
?
继续延续前面的思想——列表中的成员位置引用了对象。如图5-3-3所示,列表 lst3
和 lst4
的索引 2
的位置都引用了同一个列表对象。
图5-3-3 列表中含列表
当执行注释(5)之后,向该列表对象 [3, 4]
中追加了一个整数 9
,且此列表对象原地修改,即 lst3
和 lst4
的索引 2
的位置所引用的对象没有变——变的是它里面的成员。因此,列表 lst4
不再是 [1, 2, [3, 4]]
,而是:
>>> lst4
[1, 2, [3, 4, 9]]
与此类似,字典、集合中的成员与相应的对象之间都是“引用”关系,在执行 cop()
方法时也会看到类似以上列表的现象,例如:
>>> 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节)将列表、字典、集合三个容器“浅拷贝”前后的成员引用对象的内存地址打印出来,从中进一步理解上述“浅拷贝”的含义。
# 列表
>>> 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()
方法就无能为力了,必须使用另外一个专门工具。
>>> 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
不是同一个对象,但这还不能说明它是否连同容器内部的对象都复制了一份。
>>> {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 内置对象,它们是以后编写程序的基础,务必要熟练掌握。为此,提出以下建议供读者参考: