6.18 元组的特殊特性

6.18.1 不可变性给元组带来了什么影响

是的,我们在好多地方使用到了“不可变性”这个单词,除了这个词的计算机学科定义和实现,从应用的角度来考虑,这个词的底线是什么?一个数据类型成为不可变的到底意味着什么?

在3个标准不可变类型里面——数字、字符串和元组字符串——元组是受到影响最大的,一个数据类型是不可变的,简单来讲,就意味着一旦一个对象被定义了,它的值就不能再被更新,除非重新创建一个新的对象。对数字和字符串的影响不是很大,因为它们是标量类型,当它们代表的值改变时,这种结果是有意义的,是按照你所想要的方式进行访问的。而对于元组,事情就不是这样了。

因为元组是容器对象,很多时候你想改变的只是这个容器中的一个或者多个元素。不幸的是这是不可能的,切片操作符不能用作左值进行赋值。这和字符串没什么不同,切片操作只能用于只读的操作。

不可变并不是坏事,比如我们把数据传给一个不了解的API时,可以确保我们的数据不会被修改。同样地,如果我们操作从一个函数返回的元组,可以通过内建list()函数把它转换成一个列表。

6.18.2 元组也不是那么“不可变”

虽然元组是被定义成不可变的,但这并不影响它的灵活性。元组并不像我们想的那么不可变,这是什么意思?其实元组几个特定的行为让它看起来并不像我们先前声称的那么不可变。

比如说,既然我们可以把字符串组合在一起形成一个大字符串。那么把元组组合在一起形成一个大的元组也没什么不对。所以,连接操作可用,这个操作一点都没有改变那些小元组。我们所作的是把它们的元素结合在一起。这里有几个例子。

6.18 元组的特殊特性 - 图1

6.18 元组的特殊特性 - 图2

同样的概念也适用于重复操作。重复操作只不过是多次复制同样的元素。再有,我们前面提到过可以用一个简单的函数调用把一个元组变成一个可变的列表。我们的最后一个特性可能会吓到你。你可以“修改”特定的元组元素,哇!这意味着什么?

虽然元组对象本身是不可变的,但这并不意味着元组包含的可变对象也不可变了。

6.18 元组的特殊特性 - 图3

在上面的例子中,虽然t是一个元组类型变量,但是我们设法通过替换它的第一个元素(一个列表对象)的项来“改变”了它。我们替换了t[0][1],原来是个整型,我们把它替换成了一个列表对象[‘abc’,‘def’]。虽然我们只是改变了一个可变对象,但在某种意义上讲,我们也“改变”了我们的元组类型变量。

6.18.3 默认集合类型

所有的多对象的、逗号分隔的、没有明确用符号定义的(比如用方括号表示列表和用圆括号表示元组),这些集合默认的类型都是元组。下面是一个简单的示例。

6.18 元组的特殊特性 - 图4

所有函数返回的多对象(不包括有符号封装的)都是元组类型。注意,有符号封装的多对象集合其实是返回的一个单一的容器对象,比如:

6.18 元组的特殊特性 - 图5

上面的例子中,fool()返回3个对象,默认的作为一个3元组类型;foo2()返回一个单一对象,一个包含3个对象的列表;foo3()返回一个跟fool()相同的对象。唯一不同的是这里的元组是显式定义的。

为了避免令人讨厌的副作用,建议总是显式地用圆括号表达式表示元组或者创建元组。

6.18 元组的特殊特性 - 图6

6.18 元组的特殊特性 - 图7

在第1个例子中小于号的优先级高于逗号,2<3的结果成了元组变量的第2个元素,适当地封装元组就会得到希望得到的结果。

6.18.4 单元素元组

曾经试过创建一个只有一个元素的元组?你在列表上试过,它可以完成,但是无论你怎么在元组上试验,都不能得到想要的结果。

6.18 元组的特殊特性 - 图8

或许你忘记了圆括号被重载了,它也被用作分组操作符。由圆括号包裹的一个单一元素首先被作为分组操作,而不是作为元组的分界符。一个变通的方法是在第一个元素后面添一个逗号(,)来表明这是一个元组而不是在做分组操作。

6.18 元组的特殊特性 - 图9

6.18.5 字典的关键字

不可变对象的值是不可改变的。这就意味着它们通过hash算法得到的值总是一个值。这是作为字典键值的一个必备条件。在下一章节里面我们会讨论到,键值必须是可“hash”的对象,元组变量符合这个标准,而列表变量就不行。

6.18 元组的特殊特性 - 图10核心笔记:列表VS.元组

一个经常会被问到的问题是,“为什么我们要区分元组和列表变量?”这个问题也可以被表述为“我们真的需要两个相似的序列类型吗?”,一个原因是在有些情况下,使用其中的一种类型要优于使用另一种类型。

最好使用不可变类型变量的一个情况是,如果你在维护一些敏感的数据,并且需要把这些数据传递给一个并不了解的函数(或许是一个根本不是你写的API),作为一个只负责一个软件某一部分的工程师,如果你确信你的数据不会被调用的函数篡改,你会觉得安全了许多。

一个需要可变类型参数的例子是,在管理动态数据集合时。你需要先把它们创建出来,逐渐地或者不定期地添加它们,或者有时还要移除一些单个的元素。这是一个必须使用可变类型对象的典型例子。幸运的是,通过内建的list()和tuple()转换函数,你可以非常轻松地在两者之间进行转换。

list()和tuple()函数允许你用一个列表来创建一个元组,反之亦然。如果你有一个元组变量,但你需要一个列表变量,因为你要更新一下它的对象,这时list()函数就是你最好的帮手。如果你有一个列表变量,并且想把它传递给一个函数,或许一个API,而你又不想让任何人弄乱你的数据,这时tuple()函数就非常有用。