由 Al Sweigart 发布
'When I use a word,' Humpty Dumpty said, in rather a scornful tone, 'it means just what I choose it to mean — neither more nor less.'
'The question is,' said Alice, 'whether you can make words mean so many different things.'
'The question is,' said Humpty Dumpty, 'which is to be master — that's all.'
“当我用一个词的时候,”汉普蒂·邓普蒂轻蔑地说,“它的意思正是我选择它的意思——不多也不少。”
“问题是,”爱丽丝说,“你能不能用词来表达这么多不同的意思。”
“问题是,”汉普蒂·达普蒂说,“这就是主人——就这些。”
节选文章:http://www.alice-in-wonderland.net/resources/chapters-script/through-the-looking-glass/chapter-6/
在 Python 中,元组是不可变的,“不可变的”意味着值不会发生改变。这些是众所周知的 Python 的基本事实。然而元组不仅仅是不可变的列表(正如 Luciano Ramalho 优秀的《Fluent Python》第二章解释的那样),这里有一些含糊之处。Luciano 也写了一篇关于这个主题的精彩博文。
在我们得到关于“元组是可变的还是不可变的?”的一个细致入微的答案之前,我们需要一些背景信息。
根据 Python 数据模型(Python data model),“对象是 Python 的数据抽象,Python 程序中的所有数据都由对象或对象之间的关系表示”。在 Python 中每个值是对象,包括整型,浮点数和布朗值。在 Java 中,这些是“原始数据类型(primitive data types)”,被认为与“对象”分开。在 Python 中,却不是这样的。在 Python 中每个值是对象,而且 Ned Batchelder 精彩的 PyCon 2015 演讲 Facts and Myths About Python Names and Values 详细介绍了这一点。
所以不仅 datetime 对象【datetime.datetime(2018, 2, 4, 19, 38, 54, 798338)】是对象,整型 42 和布朗值 True 也是对象。
所有的 Python 对象有三样东西:值(value),类型(type)和身份/标识(identity)。这有一点困惑,因为我们经常漫不经心地说,例如,“值 42”,尽管 42 也被称为本身具有一个值对象。但是不要紧,我们继续我们 42 的例子。在交互式 shell 中输入以下内容:
>>> spam = 42
>>> spam
42
>>> type(spam)
>>> id(spam)
1594282736
变量 spam 引用一个对象,对象的为 42,类型为 int,身份/标识为 1594282736。身份/标识(identity)是一个独一无二的整型(ingeter),当对象被创建时它被创建,在对象的生命周期中永远也不会改变。一个对象的类型也不会改变。只有对象的值可能改变。
让我们尝试改变对象的值通过在交互式 shell 中输入以下内容:
>>> spam = 42
>>> spam = 99
你可能认为你已经将对象的值从 42 改变为 99,但是事实上你并没有。所以你做过的事情只是把变量 spam 指向了一个新的对象。你可以通过调用 id() 函数确认这一事实,而且注意到变量 spam 完全指向了一个新的对象:
>>> spam = 42
>>> id(spam)
1594282736
>>> spam = 99
>>> id(spam)
1594284560
整型(浮点数,布朗值,字符串,固定集合【frozen sets】和字节【bytes】)是不可变的;它们的值不会改变。另一方面,列表(字典,集合,数组【arrays】和字节数组【bytearrays】)是可变的。这可能导致一个常见的 Python 陷阱。
>>> spam = ['dogs', 'cats']
>>> eggs = spam # 只复制了对列表的引用,而不是列表本身
>>> spam
['dogs', 'cats']
>>> eggs
['dogs', 'cats']
>>> spam.append('moose') # 修改变量 spam 引用的列表
>>> spam
['dogs', 'cats', 'moose']
>>> eggs
['dogs', 'cats', 'moose']
即时我们仅仅给变量 spam 附加(append)一个值,变量 eggs 改变的原因是变量 spam 和 eggs 引用了同一个对象。eggs = spam 这一行复制了对象的引用,并不是对象本身(如果你想要赋值列表对象,你可以使用模块的 copy() 或 deepcopy() 函数)
Python官方文档中的术语表说“不可变的”(着重强调我的):
"An object with a fixed value. Immutable objects include numbers, strings and tuples. Such an object cannot be altered. A new object has to be created if a different value has to be stored."
“对象具有固定的值。不可变的对象包括数字,字符串和元组。这样的对象是不能改变的。如果必须储存不同的对象,就必须创建新对象。”
官方Python文档(以及我发现的所有其他书籍,教程和 StackOverflow 上的答案)都将远足描述为不可变的。相反术语表中说这是“可变的”
"Mutable objects can change their value but keep their id(). See also immutable."
“可变对象可以改变它们的值,但保留它们的 id()。另见不可变的。”
让我们继续讨论,值(value)和身份(identity)如何影响 == 和 is 运算符。
== 平等操作符比较直,而 is 操作符比较身份。你可以把 x is y 认为是 id(x) == id(y) 的简写。在交互式 shell 中输入以下内容:
>>> spam = ['dogs', 'cats']
>>> id(spam)
41335048
>>> eggs = spam
>>> id(eggs)
41335048
>>> id(spam) == id(eggs)
True
>>> spam is eggs # spam 和 eggs 是相同的对象
True
>>> spam == eggs # spam 和eggs 本质上具有相同的值
True
>>> spam == spam # 就像 spam 和 spam 是同一个对象,自然也有相同的值
True
>>> bacon = ['dogs', 'cats']
>>> spam == bacon # spam 和 bacon 有相同的值
True
>>> id(bacon)
40654152
>>> id(spam) == id(bacon)
False
>>> spam is bacon # spam 和 bacon 是不同的对象
False
两个不同的对象可以共享相同值,但是它们永远不能共享相同的身份/标识(identity)。
根据官方Python文档的术语表中,“如果一个对象具有一个在其生命周期内永远不会改变的哈希值,那么该对象是可哈希的("An object is hashable if it has a hash value which never changes during its lifetime")”,也就是说,如果对象是不可变的。(关于__hash__() 和 __eq__() 特殊方法还有其他一些要求,但这超出了本文的范围。)
哈希是一个依赖于对象的值的整型,具有相同的值的对象总是具有相同的哈希。(具有不同数值的对象有时也有相同的哈希。这个被称为哈希冲突【hash collision】)。id() 会返回基于对象身份的整型,而 hash() 函数会返回基于可哈希对象值的整型(对象的哈希):
>>> hash('dogs')
-4064183437113369969
>>> hash(True)
1
>>> spam = ('hello', 'goodbye')
>>> eggs = ('hello', 'goodbye')
>>> spam == eggs # spam 和 eggs 有相同的值
True
>>> spam is eggs # spam 和 eggs 是具有不同身份/标识(identity)的不同对象
False
>>> hash(spam)
3746884561951861327
>>> hash(eggs)
3746884561951861327
>>> hash(spam) == hash(eggs) # spam 和 eggs 具有相同的哈希
True
不可变对象可以被哈希,可变对象不能被哈希。这一点很重要,因为(原因超出本文范围)只有哈希的对象可以用作字典中的键或者集合中的项。由于哈希基于值而且只有不可变对象能被哈希,这意味着在对象的生命周期中哈希永远不会被改变。
在交互式shell中,尝试通过输入以下内容为键创建一个不可变对象的字典。
>>> spam = {'dogs': 42, True: 'hello', ('a', 'b', 'c'): ['hello']}
>>> spam.keys()
dict_keys(['dogs', True, ('a', 'b', 'c')])
在变量 spam 中所有的键都是不可变的(immutable),可哈希的对象。如果你尝试在可变对象(例如列表)上调用 hash() 函数,或尝试把可变对象用作为字典的键,你会得到一个错误(error):
>>> spam = {['hello', 'world']: 42}
Traceback (most recent call last):
File "", line 1, in
TypeError: unhashable type: 'list'
>>> d = {'a': 1}
>>> spam = {d: 42}
Traceback (most recent call last):
File "", line 1, in
TypeError: unhashable type: 'dict'
作为不可变对象的元组可以用作为字典的键:
>>> spam = {('a', 'b', 'c'): 'hello'}
。。。或者它们可以么?
>>> spam = {('a', 'b', [1, 2, 3]): 'hello'}
Traceback (most recent call last):
File "", line 1, in
TypeError: unhashable type: 'list'
看上去如果元组包含可变对象,它是不能被哈希的。(Raymond Hettinger 用大量的上下文(context)解释道了为什么不可变的元组可以包含可变的值。)这符合我们所知道的,不可变对象可以被哈希,但是这并意味着它们总是可被哈希的。请记住,哈希是源自于对象的值(value)。
这是一个有趣的角落案例:本应该是不可变的元组包含了可变的列表是不能被哈希的。这是因为元组的哈希依赖于元组的值,但是如果那个列表的值改变了,这意味着元组的值可以改变,因此哈希可以在元祖的生命周期中改变。但是如果我们回到哈希的术语表定义处,在对象的生命周期中,哈希永远不应该被改变。
这是否意味着元组的值可以改变?元组是否可变?
在我们能够最终回答这个问题之前,我们应该问,“可变性是数据类型(data types)还是对象(objects)的属性(property)”
(更新:我们BDFL通常认为它是数据类型。)
Python程序员经常说“字符串是不可变的,列表是可变的”,这使我们认为可变性是类型(types)的属性:所有字符串对象都是不可变的,所有列表对象都是可变的等等。总的来说,我同意。
但是在上一节中,我们见证了一些元组是可哈希的(暗示它们是不可变的),但是一些其它的元组是不可哈希的(暗示它们是可变的)。
让我们回到官方Python文档的不可变和可变的定义:分别是“具有固定值的对象”和“可变对象可以改变它们的值”。
因此,可能可变性(mutability)是对象(objects)的一个属性,一些元组(只包含不可变对象)是不可变的(immutable),而其他一些元组(包含一个或多个可变对象)是可变的(mutable)。但我遇到每一个 Pythonista 的都会说,而且继续说,元组是不变的,即便它们是不可哈希的。这是为什么呢?
从某种意义上说,元组是不可变的,因为元组中的对象不能被新对象删除或替换。就像 spam = 42; spam = 99 不会改变 spam 中的对象 42;它用全新的对象(99)取而代之。如果我们使用交互式 shell 来查看包含列表的元组:
>>> spam = ('dogs', 'cats', [1, 2, 3])
>>> id(spam[0]), id(spam[1]), id(spam[2])
(41506216, 41355896, 40740488)
相同的对象将始终位于此元组中,并且它们将始终以相同的顺序具有相同的身份: 41506216, 41355896, and 40740488. 元组是不可变的(immutable)。
但从另一个意义上讲,元组是可变的,因为它们的值可以改变。在交互式shell中输入以下内容:
>>> a = ('dogs', 'cats', [1, 2, 3])
>>> b = ('dogs', 'cats', [1, 2, 3])
>>> a == b
True
>>> a is b
False
在本例中,a 和 b 引用的元组具有相等的值(根据 ==),但它们是不同的对象(根据 is)。让我们更改 a 的元组中的列表:
>>> a[2].append(99)
>>> a
('dogs', 'cats', [1, 2, 3, 99])
>>> a == b
False
我们改变了 a 的值。我们一定是改变了,因为 a 不再等于 b 而且我们没有改变 b 的值。元组是可变的(mutable)。
'The question is,' said Humpty Dumpty, 'which is to be master — that's all.'
“问题是,”汉普蒂·邓普蒂说,“这就是主人 - 这就是全部。”
人类是言语的主人,而不是相反。人类发明的词语是为了向其他人传达思想。就我个人而言,我将继续说元组是不可变的,因为在大多数情况下,这更准确,也是最有用的术语。
然而,同时我们也应该意识到,从我们对“可变的(mutable)”和“值(value)”的定义中,我们可以看到元组的值有时会发生变化,即变异。从技术上讲,元组可以是可变的并不是错误的,尽管它肯定会引起人们的注意,并且需要更多的解释。
感谢您坐在我身边,对元组和可变性进行了冗长的解释。我当然在写这篇文章时学到了很多东西,希望我也把它传达给你。
转载:https://inventwithpython.com/blog/2018/02/05/python-tuples-are-immutable-except-when-theyre-mutable/
作者:Al Sweigart