4.1 回到基础

赋值

赋值似乎是最基本的编程概念,不值得单独讨论。不过,也有一些令人吃惊的微妙之处。思考下面的代码片段:

  1. >>> foo = 'Monty'
  2. >>> bar = foo ![[1]](/projects/nlp-py-2e-zh/Images/ffa808c97c7034af1bc2806ed7224203.jpg)
  3. >>> foo = 'Python' ![[2]](/projects/nlp-py-2e-zh/Images/aa68e0e8f4d58caa31e5542dabe4ddc2.jpg)
  4. >>> bar
  5. 'Monty'

这个结果与预期的完全一样。当我们在上面的代码中写bar = foo[1]foo的值(字符串'Monty')被赋值给bar。也就是说,barfoo的一个副本,所以当我们在第[2]行用一个新的字符串'Python'覆盖foo时,bar的值不会受到影响。

然而,赋值语句并不总是以这种方式复制副本。赋值总是一个表达式的值的复制,但值并不总是你可能希望的那样。特别是结构化对象的“值”,例如一个列表,实际上是一个对象的引用。在下面的例子中,[1]foo的引用分配给新的变量bar。现在,当我们在[2]行修改foo内的东西,我们可以看到bar的内容也已改变。

  1. >>> foo = ['Monty', 'Python']
  2. >>> bar = foo ![[1]](/projects/nlp-py-2e-zh/Images/ffa808c97c7034af1bc2806ed7224203.jpg)
  3. >>> foo[1] = 'Bodkin' ![[2]](/projects/nlp-py-2e-zh/Images/aa68e0e8f4d58caa31e5542dabe4ddc2.jpg)
  4. >>> bar
  5. ['Monty', 'Bodkin']

Images/array-memory.png

图 4.1:列表赋值与计算机内存:两个列表对象foobar引用计算机内存中的相同的位置;更新foo将同样修改bar,反之亦然。

bar = foo[1]行并不会复制变量的内容,只有它的“引用对象”。要了解这里发生了什么事,我们需要知道列表是如何存储在计算机内存的。在4.1中,我们看到一个列表foo是对存储在位置 3133 处的一个对象的引用(它自身是一个指针序列,其中的指针指向其它保存字符串的位置)。当我们赋值bar = foo时,仅仅是 3133 位置处的引用被复制。这种行为延伸到语言的其他方面,如参数传递(4.4)。

让我们做更多的实验,通过创建一个持有空列表的变量empty,然后在下一行使用它三次。

  1. >>> empty = []
  2. >>> nested = [empty, empty, empty]
  3. >>> nested
  4. [[], [], []]
  5. >>> nested[1].append('Python')
  6. >>> nested
  7. [['Python'], ['Python'], ['Python']]

请看,改变列表中嵌套列表内的一个项目,它们全改变了。这是因为三个元素中的每一个实际上都只是一个内存中的同一列表的引用。

注意

轮到你来: 用乘法创建一个列表的列表:nested = [[]] * 3。现在修改列表中的一个元素,观察所有的元素都改变了。使用 Python 的id()函数找出任一对象的数字标识符, 并验证id(nested[0])id(nested[1])id(nested[2])是一样的。

现在请注意,当我们分配一个新值给列表中的一个元素时,它并不会传送给其他元素:

  1. >>> nested = [[]] * 3
  2. >>> nested[1].append('Python')
  3. >>> nested[1] = ['Monty']
  4. >>> nested
  5. [['Python'], ['Monty'], ['Python']]

我们一开始用含有 3 个引用的列表,每个引用指向一个空列表对象。然后,我们通过给它追加'Python'修改这个对象,结果变成包含 3 个到一个列表对象['Python']的引用的列表。下一步,我们使用到一个新对象['Monty']的引用来 覆盖 三个元素中的一个。这最后一步修改嵌套列表内的 3 个对象引用中的 1 个。然而,['Python']对象并没有改变,仍然是在我们的嵌套列表的列表中的两个位置被引用。关键是要明白通过一个对象引用修改一个对象与通过覆盖一个对象引用之间的区别。

注意

重要: 要从列表foo复制项目到一个新的列表bar,你可以写bar = foo[:]。这会复制列表中的对象引用。若要复制结构而不复制任何对象引用,请使用copy.deepcopy()

等式

Python 提供两种方法来检查一对项目是否相同。is操作符测试对象的 ID。我们可以用它来验证我们早先的对对象的观察。首先,我们创建一个列表,其中包含同一对象的多个副本,证明它们不仅对于==完全相同,而且它们是同一个对象:

  1. >>> size = 5
  2. >>> python = ['Python']
  3. >>> snake_nest = [python] * size
  4. >>> snake_nest[0] == snake_nest[1] == snake_nest[2] == snake_nest[3] == snake_nest[4]
  5. True
  6. >>> snake_nest[0] is snake_nest[1] is snake_nest[2] is snake_nest[3] is snake_nest[4]
  7. True

现在,让我们将一个新的 python 放入嵌套中。我们可以很容易地表明这些对象不完全相同:

  1. >>> import random
  2. >>> position = random.choice(range(size))
  3. >>> snake_nest[position] = ['Python']
  4. >>> snake_nest
  5. [['Python'], ['Python'], ['Python'], ['Python'], ['Python']]
  6. >>> snake_nest[0] == snake_nest[1] == snake_nest[2] == snake_nest[3] == snake_nest[4]
  7. True
  8. >>> snake_nest[0] is snake_nest[1] is snake_nest[2] is snake_nest[3] is snake_nest[4]
  9. False

你可以再做几对测试,发现哪个位置包含闯入者,函数id()使检测更加容易:

  1. >>> [id(snake) for snake in snake_nest]
  2. [4557855488, 4557854763, 4557855488, 4557855488, 4557855488]

这表明列表中的第二个项目有一个独特的标识符。如果你尝试自己运行这段代码,请期望看到结果列表中的不同数字,以及闯入者可能在不同的位置。

有两种等式可能看上去有些奇怪。然而,这真的只是类型与标识符式的区别,与自然语言相似,这里在一种编程语言中呈现出来。

条件

if语句的条件部分,一个非空字符串或列表被求值为真,而一个空字符串或列表的被求值为假。

  1. >>> mixed = ['cat', '', ['dog'], []]
  2. >>> for element in mixed:
  3. ... if element:
  4. ... print(element)
  5. ...
  6. cat
  7. ['dog']

也就是说,我们 不必 在条件中写if len(element) > 0:

使用if...elif而不是在一行中使用两个if语句有什么区别?嗯,考虑以下情况:

  1. >>> animals = ['cat', 'dog']
  2. >>> if 'cat' in animals:
  3. ... print(1)
  4. ... elif 'dog' in animals:
  5. ... print(2)
  6. ...
  7. 1

因为表达式中if子句条件满足,Python 就不会求值elif子句,所以我们永远不会得到输出2。相反,如果我们用一个if替换elif,那么我们将会输出12。所以elif子句比单独的if子句潜在地给我们更多信息;当它被判定为真时,告诉我们不仅条件满足而且前面的if子句条件 满足。

all()函数和any()函数可以应用到一个列表(或其他序列),来检查是否全部或任一项目满足某个条件:

  1. >>> sent = ['No', 'good', 'fish', 'goes', 'anywhere', 'without', 'a', 'porpoise', '.']
  2. >>> all(len(w) > 4 for w in sent)
  3. False
  4. >>> any(len(w) > 4 for w in sent)
  5. True