共享引用——共享引用和就地修改

未匹配的标注

在这部分章节的后面会看到:有对象和操作执行就地的对象修改——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开始也有这个方法),或将原来的对象传递给它们的类型名称函数:dictset。还有,注意标准库copy模块有一个调用可以通用地拷贝任何对象类型,还有一个调用可以拷贝嵌套的对象结构——比如,一个带有嵌套列表的字典:

import copy
X = copy.copy(Y) # 创建任意对象Y的顶层“浅”拷贝
X = copy.deepcopy(Y) # 创建任意对象Y的深拷贝: 拷贝所有嵌套部分

第8章第9章,将深入探索列表和字典,重温共享引用和拷贝的概念。就现在而言,记住:能被就地修改的对象(也就是:可变的对象)在任何它们通过的代码中对这种类型的效果总是开放的(译注:未受保护的,容易被影响的)。在Python中,这些对象包括列表、字典、sets和一些用class语句定义的对象。如果这不是想要的行为,可以简单地按需拷贝对象。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 查看所有版本


暂无话题~