17: 存在

这节课提供更多有关Python储存序列的信息。复制并比较序列会有预料之外的结果。我们会解释Python内部是如何运行的,让你理解并避免这些错误。比如,一个普遍的问题是几个不同的变量会“表明”或“指向”相同的序列。这节课的末尾我们会引入"is"运算符。这个运算符是用来判断是否多个变量确实指向同一个序列。

范例

我们想要一串代码,将一个英尺为单位的长度序列oldSize转变为一个以厘米为单位的长度序列newSize。其中一种方式是使用newSize = oldSize来建立一份副本,然后接着往下改变所有的值:

oldSize = ["letter", 8.5, 11]   # size of paper in inches
newSize = oldSize               # make a copy?
newSize[1] = newSize[1]*2.54    # convert to cm
newSize[2] = newSize[2]*2.54    # convert to cm
现在让我们看看是否能正常运行。如果你打印newSize,那么你会得到["letter", 21.59, 27.94]。注意,当我们打印oldSize的时候,oldSize的值会改变!下面是具体解释:

示例
第二行输出不是我们想要的!

我们会在下面通过使用图表详细解释刚刚发生了什么。其实这里的主要问题是newSize = oldSize没有复制整个序列:它只是将参考(箭头)复制到了相同的序列上。

点击标题改变标签。


Memory
我们会使用一个表格来表示Python内存中的变量和值。比如,在运行下列代码后

city = "Moose Factory"
population = 2458
employer = city
这个表格(带黑色边框的)展示了Python 的内存:

第一个变量的名字是city,值为字符串"Moose Factory"。第二个变量的名字是population,值是整数2458。第三个变量名字是employer,值是字符串"Moose Factory"
Lists in memory
接下来,我们会展示序列在内存中是什么样的。比如,下列代码

myList = ["Moose Factory", 2458]
只会创建一个变量,名为myList。一个序列被创建,myList的值"指向"或"对应"那个序列。我们用一个盒子代表序列,序列的项在盒子里紧挨着他们对应的索引。序列是蓝色的。

箭头表明myList指向新的序列。序列中索引为0的那一项是字符串"Moose Factory",索引为1的那一项是整数2458。比如,如果你print(myList[1]),那么Python会输出2458
替换序列的值
为了说得更清楚,我们在上一个例子中增加一行。我们运行以下代码

myList = ["Moose Factory", 2458]
myList[1] = myList[1]+1
Python计算2458+1,结果是2459,这个会替换序列中索引为1的项。更新后,我们得到以下图表。

(划掉的2458 是只是为了强调这个改变。)
oldSize and newSize
现在我们回到第一个例子中。第一行原来是

oldSize = ["letter", 8.5, 11]
这一行运行后,Python的内存就如以下图表所示。我们创建了一个长为3的序列。
Main problem
在我们的程序中,第二行是

newSize = oldSize
这节课最重要的一点是:=并不复制序列!相反,它只创建了一个新的指向,指向同一个序列。这可以用两个不同的箭头指向同一个盒子表明。
如下表所示,我们有两个变量,两个都指向同一个序列。
Updating
接着,当程序达到

newSize[1] = newSize[1]*2.54
Python会查看newSize,查看索引为1的值(8.5),并乘2.54,然后代替原来的值。然而,因为oldSize指向相同的序列,我们也影响了oldSize指向的值!

(同样,划掉的8.5 只是为了强调这个改变。)
Result
下一行也是同样的道理,

newSize[2] = newSize[2]*2.54
影响序列中的另一个值。在这一行运行后,Python的内存就如下图所示。

现在当我们打印newSize oldSize,Python输出["letter", 21.59, 27.94]

为了查看没有评论的图片,在可视化窗口中运行相同的代码

Python 中的每一个值都是位于特定内存块中的对象。并且每个变量只是一个参考/指针/箭头。比如,在第一个标签中,内存只包含一个"Moose Factory",两个箭头指向它。因为数字和字符串是"不可改变的",我们在这些课中不用箭头画它们。但是在后台,Python对所有的数据一视同仁。详情

Python中的"深度"复制序列

在上述例子中,我们使用=来在现存的序列中创建第二指针/参考/箭头,通常被叫做"浅"复制。尽管它是虽然有时这么做有用,但在此时这并不是我们想要的。反而,我们想创建一个全新的序列副本。那么该如何完成呢?

我们给出三个答案。它们基本上相同,但是每一个都会告诉你一个关于Python的新知识,所以三个都值得阅读。

方法1,使用[:]

示例
使用[:]复制

在上述例子中,我们证明了newList = oldList[:]创建了原来序列的副本。尽管这个句法看起来很陌生,但其实我们见到过类似的东西。在字符串课程中,我们介绍了一种分离子链的方式:string[first:tail]返还子链,从索引first开始,直到索引tail-1结束。我们曾经提到这还可以应用于创建序列的子序列。除此以外,

  • 如果你省略first,然后取默认值0
  • 如果你省略tail,然后取len的默认值(序列/字符串的长度)。

所以实际上,oldList[:]创建一个新的子序列。这个子序列与原序列相同,是一份新的副本。

方法2,使用copy.copy()

有一个模块叫做copy,里面有几个关于复制的方法。最简单的一个叫做copy.copy(L):当你对序列L调用这个程序时,程序会返还L真正的副本。

示例
使用copy.copy复制

copy.copy()函数也对其他种类的对象适用,但在这里我们先不讨论。

方法3,使用list()

最后一种得到真正副本的方式是使用list()方程。本来,list()是被用来将其它类型的数据转化成list种类(比如,list("hello") 字符串"hello"转化为一个有五项的序列,每一项是一个字符)。但是如果你想要将一个序列转化成一个序列,它只会创建一个副本。

示例
使用list()复制

序列作为参数

注意,因为序列本身运行的方式,任何将序列当作参数的函数都可以改变序列中的内容(在replace练习题中出现过)。

简答练习: 参数列表
下列程序输出哪个数字?

def func(list):
    list[0] = list[0] + list[1]
    list[1] = list[1] + list[0]
data = [3, 4]
func(data)
print(data[0]*data[1])
正确!在func中,索引0的数字变为7,索引1的数字变为7+4=11。所以我们打印7*11。

使用is比较序列

什么时候两个序列变量L1L2相等?我们有两种方式来转述这个问题:

  • 相同身份L1L2指向相同的序列对象么?
  • 相同值:序列L1的内容与序列L2的内容相同么?

在Python中,标准的相同运算符==意味着相同的值,如下所示。

示例
==的含义

为了测试相同身份,我们用Python中的is运算符。使用这个运算符的方式与==一样:句法

«list1» is «list2»

如果这些序列指向同一个序列,返还True;如果他们指向不同的序列(即使他们有相同的内容),返还False

示例
is的含义

简答练习: 真正的计数
这个程序的输出中True出现了多少次?(绘制一个图来帮助追踪每一步)

list1 = [9, 1, 1]
list3 = list(list1)
list2 = list1
list4 = list3[:]
list1[0] = 4
list5 = list1
print(list1 is list2, list2 is list3, list3 is list4, list4 is list5)
print(list1 == list2, list2 == list3, list3 == list4, list4 == list5)
正确!输出是 True False False False True False True False

你不应该将is用在字符串或数字中,因为 == 已经正确地测试出相等性,is很难预测字符串和数字。

嵌套序列

我们已经讲了很多,但是还有一个值得一提的常见情况。前面的课中提到过,一个嵌套序列是一个序列嵌套另一个序列,比如

sample = [365.25, ["first", 5]]

sample所指的外面的序列有两项;索引为0的项是小数,索引为1的是里面的序列。里面的序列是["first", 5](也可以有多层嵌套)。一旦你开始使用嵌套序列,记得以下几点:

  • 将上面的三个方法用于sample会复制外面的序列,而不会复制里面的序列。所以copy(sample)[1] is sample[1]意味着副本仍指向原序列中的一部分。可能这不是你想要的。如果你想在各个层次上复制,使用copy.deepcopy()
  • 使用==测试嵌套序列给了我们启发:Python重复地对序列地每一项调用==。比如,[[1, 2], 3]==[[1, 2], 3]True[[1, 2], 3]==[1, 2, 3]False,因为前几项是不一样的([1, 2] != 1)。

示例
deepcopy and recursive equality

这节课的内容到此结束!下面的内容为选学。


数组(元组):("immutable", "lists")

在上面提到过,对一个序列调用一个方程可以改变序列。有时我们并不想让这种情况发生!Python中的一种解决方式是创建数组。数组本质和序列一样,但是不可以被改动。我们说序列是"可改动的",数组是"不可改动的"(字符串和数字也是不可改动的)。这可以用来预防任何编程错误,包括误改序列。数组使用小括号()而不是中括号[]。数组和序列可以通过使用tuple()list()相互转换。

示例
管状数组(元组)

To and Beyond:不合群

创建一个包含自身的序列是可能的!只需创建一个序列,然后将其中的一项重新指回整个的序列:

示例
循环引用

注意Python的输出引擎能够识别序列自身循环: 它打印出"...",而不是重复打印所有的L,以此避免无限循环。