共享引用——共享引用和就地修改
在这部分章节的后面会看到:有对象和操作执行就地的对象修改 ——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
语句定义的对象。如果这不是想要的行为,可以简单地按需拷贝对象。
推荐文章: