共享引用——共享引用和就地修改 | 第二部分 类型与操作 —— 第 6 章: 动态类型的插曲 |《学习 python:强大的面向对象编程(第 5 版)》| python 技术论坛-金年会app官方网
在这部分章节的后面会看到:有对象和操作执行就地的对象修改——python的可变类型,包括列表、字典和sets。比如,对列表的偏移量的赋值会真正就地改变列表对象本身,而不是产生一个全新的列表对象。
然而在本书的这个节点上,必须在某种程度上无理由的相信:这种差别在程序中会非常重要。对于支持这种就地修改的对象,需要更小心共享引用,因为一个名称的改变可能影响其他的。否则,对象似乎会没有明显原因被改变。考虑到所有赋值都是基于引用(包括函数参数传递),这是一个无处不在的可能。
再看一下第4章中介绍的列表对象来展示。回忆一下:列表(支持就地赋值到位置)只是其他对象的集合,在方括号中编码:
>>> l1 = [2, 3, 4]
>>> l2 = l1
这里的l1是一个包含对象2,3,4的列表。在列表中的项是通过位置访问的,所以l[0]指的是对象2(列表l1中的第一项)。当然,列表自身也是对象,就像整数和字符串。在运行之前两个赋值后,l1和l2引用了同样的共享对象,就像之前例子中的a和b(见图6-2)。现在可以说,和之前一样,扩展交互会话如下:
>>> l1 = 24
这个赋值简单地设置l1为不同的对象;l2仍然引用原来的列表。然而,如果稍微修改下这个语句的语法,效果就会完全不同:
>>> l1 = [2, 3, 4] # 可变的对象
>>> l2 = l1 # 对同一对象创建引用
>>> l1[0] = 24 # 就地修改
>>> l1 # l1不同了
[24, 3, 4]
>>> l2 # 但l2也不同了!
[24, 3, 4]
这里真的没有修改l1本身;修改的是l1引用对象的一个组成部分。这种改变就地覆盖了列表对象值的一部分。然而,因为列表对象是被其他对象共享的(引用的形式),像这样的就地改变不只影响l1——也就是说,当进行这种改变时,必须小心它们会影响程序的其他部分。在本例中,效果在l2中也会显现出来,因为它引用了和l1相同的对象。再说一次,没有真的修改l2,但它的值将会看起来不同,因为它引用了一个已经被就地覆盖的对象。
这个行为只会在支持就地改变的可变对象上发生,且通常是你想要的,但应知道它是如何工作的,这样才不会感到意外。它也是默认的:如果不想要这种行为,可以使用python的copy
对象而非创建引用。有许多方式来拷贝列表,包括使用内置list
函数和标准库copy
模块。可能最常见的方式是从头到尾进行切片(关于切片的更多信息,请参考第4章和第7章):
>>> l1 = [2, 3, 4]
>>> l2 = l1[:] # 创建l1的拷贝 (或 list(l1), copy.copy(l1) 等等.)
>>> l1[0] = 24
>>> l1
[24, 3, 4]
>>> l2 # l2 没有改变
[2, 3, 4]
在这里,对l1的改变没有反应在l2中,因为l2引用了对象l1引用的一个拷贝,而不是原来的对象;也就是说,这两个变量指向不同的内存片段。
注意这个切片技术在其他主要的可变的核心类型(字典和sets)上不可用,因为它们不是序列——要拷贝字典或set,改用它们的 x.copy()
方法调用(列表从python3.3开始也有这个方法),或将原来的对象传递给它们的类型名称函数:dict
和set
。还有,注意标准库copy
模块有一个调用可以通用地拷贝任何对象类型,还有一个调用可以拷贝嵌套的对象结构——比如,一个带有嵌套列表的字典:
import copy
x = copy.copy(y) # 创建任意对象y的顶层“浅”拷贝
x = copy.deepcopy(y) # 创建任意对象y的深拷贝: 拷贝所有嵌套部分
在第8章和第9章,将深入探索列表和字典,重温共享引用和拷贝的概念。就现在而言,记住:能被就地修改的对象(也就是:可变的对象)在任何它们通过的代码中对这种类型的效果总是开放的(译注:未受保护的,容易被影响的)。在python中,这些对象包括列表、字典、sets和一些用class
语句定义的对象。如果这不是想要的行为,可以简单地按需拷贝对象。