Python 元组是不可变的,除非它们是可变的

序幕

“当我使用一个词时,”矮胖子用相当轻蔑的语气说,“它的意思就是我选择它的意思——不多也不少。”

“问题是,”爱丽丝说,“你是否可以让单词表示这么多不同的意思。”

“问题是,”Humpty Dumpty 说,“哪个是主人——仅此而已。”

元组是可变的还是不可变的?

在 Python 中,元组是不可变的,“不可变”意味着值不能改变。 这些是众所周知的 Python 基本事实。 虽然元组不仅仅是不可变的列表(正如 Luciano Ramalho 的优秀“Fluent Python”的第 2 章所解释的那样),但这里有一点歧义。 Luciano 也就此主题撰写了一篇很棒的博客文章。

在我们对“元组是可变的还是不可变的?”做出细致入微的回答之前,我们需要一些背景信息。

一些背景信息

根据 Python 数据模型,“对象是 Python 对数据的抽象,Python 程序中的所有数据都由对象或对象之间的关系来表示”。 Python 中的每个值都是一个对象,包括整数、浮点数和布尔值。 在 Java 中,这些是“原始数据类型”,被认为与“对象”分开。 不是这样,在 Python 中。 Python 中的每个值都是一个对象,Ned Batchelder 在 PyCon 2015 精彩演讲《关于 Python 名称和值的事实和神话》中对此进行了更详细的介绍。

所以不仅是 datetime.datetime(2018, 2, 4, 19, 38, 54, 798338) datetime object 一个对象,但是整数 42 是一个对象和布尔值 True 是一个对象。

所有 Python 对象都具有三样东西:值、类型和标识。 这有点让人迷惑,因为我们经常随便说,比如“值 42“, 虽然 42 也称为本身具有值的对象。 不过没关系,让我们继续我们的 42 例子。 在交互式 shell 中输入以下内容:

>>> spam = 42
>>> spam
42
>>> type(spam)
<class 'int'>
>>> id(spam)
1594282736

变量 spam 指的是一个对象,其值为 42, 一种 int,以及身份 1594282736. 身份是一个唯一的整数,在创建对象时创建,并且在对象的生命周期内永远不会改变。 对象的类型也不能改变。 只有对象的值可以改变。

让我们尝试通过在交互式 shell 中输入以下内容来更改对象的值:

>>> spam = 42
>>> spam = 99

您可能认为您已经将对象的值从 4299,但你没有。 你所做的一切都是成功的 spam 引用一个新对象。 您可以通过致电 id() 功能和注意 spam 指的是一个全新的对象:

>>> spam = 42
>>> id(spam)
1594282736
>>> spam = 99
>>> id(spam)
1594284560

整数(以及浮点数、布尔值、字符串、冻结集和字节)是不可变的; 他们的价值不会改变。 另一方面,列表(以及字典、集合、数组和字节数组)是可变的。 这可能会导致一个常见的 Python 问题:

>>> spam = ['dogs', 'cats']
>>> eggs = spam  # copies the reference, not the list

>>> spam
['dogs', 'cats']
>>> eggs
['dogs', 'cats']

>>> spam.append('moose') # modifies the list referred by spam
>>> spam
['dogs', 'cats', 'moose']
>>> eggs
['dogs', 'cats', 'moose']

原因 eggs 已经改变了,即使我们只附加了一个值 spam 是因为 spameggs 引用同一个对象。 这 eggs = spam 行复制了引用,而不是对象。 (您可以使用 copy 模块的 copy() 或者 deepcopy() 如果你想复制一个列表对象的功能。)

Python 官方文档中的术语表是关于“不可变”的(强调我的):

“具有固定值的对象。不可变对象包括数字、字符串和元组。这样的对象不能更改。如果必须存储不同的值,则必须创建一个新对象。”

官方 Python 文档(以及我找到的所有其他书籍、教程和 StackOverflow 答案)将元组描述为不可变的。 相反,词汇表是关于“可变”的:

“可变对象可以改变它们的值但保持它们的 id(). 另见不可变的。”

让我们继续讨论价值和身份如何影响 ==is 运营商。

== 对比 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 and eggs are the same object
True
>>> spam == eggs  # spam and eggs have the same value, naturally
True
>>> spam == spam  # Just like spam and spam are the same object and have the same value, naturally
True

>>> bacon = ['dogs', 'cats']
>>> spam == bacon  # spam and bacon have the same value
True
>>> id(bacon)
40654152
>>> id(spam) == id(bacon)
False
>>> spam is bacon  # spam and bacon are different objects
False

两个不同的对象可能共享相同的值,但它们永远不会共享相同的标识。

可哈希性

根据 Python 官方文档中的词汇表,“如果对象具有在其生命周期内永不改变的哈希值,则该对象是可哈希的”,也就是说,如果对象是不可变的。 (还有一些其他要求 __hash__()__eq__() 特殊方法,但这超出了本文的范围。)

散列是一个取决于对象值的整数,具有相同值的对象总是具有相同的散列。 (具有不同值的对象有时也会具有相同的散列。这称为散列冲突。)虽然 id() 将根据对象的身份返回一个整数, hash() 函数将根据可散列对象的值返回一个整数(对象的散列):

>>> hash('dogs')
-4064183437113369969

>>> hash(True)
1

>>> spam = ('hello', 'goodbye')
>>> eggs = ('hello', 'goodbye')
>>> spam == eggs  # spam and eggs have the same value
True
>>> spam is eggs  # spam and eggs are different objects with different identities
False
>>> hash(spam)
3746884561951861327
>>> hash(eggs)
3746884561951861327
>>> hash(spam) == hash(eggs)  # spam and eggs have the same hash
True

不可变对象可以是可哈希的,可变对象不能是可哈希的。 了解这一点很重要,因为(出于超出本文范围的原因)只有可哈希对象才能用作字典中的键或集合中的项。 由于散列是基于值的,并且只有不可变对象才可以散列,这意味着散列在对象的生命周期内永远不会改变。

在交互式 shell 中,尝试通过输入以下内容来为键创建一个包含不可变对象的字典:

>>> spam = {'dogs': 42, True: 'hello', ('a', 'b', 'c'): ['hello']}
>>> spam.keys()
dict_keys(['dogs', True, ('a', 'b', 'c')])

所有的钥匙在 spam 是不可变的、可散列的对象。 如果你试着打电话 hash() 在可变对象(如列表)上,或尝试使用可变对象作为字典键,你会得到一个错误:

>>> spam = {['hello', 'world']: 42}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

>>> d = {'a': 1}
>>> spam = {d: 42}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'

元组是不可变对象,可以用作字典键:

>>> spam = {('a', 'b', 'c'): 'hello'}

…或者他们可以吗?:

>>> spam = {('a', 'b', [1, 2, 3]): 'hello'}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

似乎如果一个元组包含一个可变对象,它就不能被散列。 (Raymond Hettinger 在大量上下文中解释了为什么不可变元组可以包含可变值。)这符合我们所知道的:不可变对象可以是可哈希的,但这并不一定意味着它们总是可哈希的。 请记住,散列是从对象的值派生的。

这是一个有趣的极端情况:不能对包含可变列表的元组(应该是不可变的)进行哈希处理。 这是因为元组的散列取决于元组的值,但如果该列表的值可以更改,则意味着元组的值可以更改,因此散列可以在元组的生命周期内更改。 但是,如果我们回到可散列的词汇表定义,散列在对象的生命周期内永远不会改变。

这是否意味着元组的值可以改变? 元组是可变的吗?

那么,元组是可变的还是不可变的?

在我们最终回答这个问题之前,我们应该问,“可变性是数据类型或对象的属性吗?”

(更新:我们的 BDFL 通常认为它是数据类型的。)

Python 程序员经常说“字符串是不可变的,列表是可变的”之类的话,这让我们认为可变性是类型的属性:所有字符串对象都是不可变的,所有列表对象都是可变的,等等。总的来说,我同意。

但是在上一节中,我们看到了一些元组是如何可哈希的(暗示它们是不可变的)而其他一些元组是不可哈希的(暗示它们是可变的)。

让我们回到 Python 官方文档对不可变和可变的定义:分别是“具有固定值的对象”和“可变对象可以改变它们的值”。

因此,也许可变性是对象的一种属性,一些元组(仅包含不可变对象)是不可变的,而其他一些元组(包含一个或多个可变对象)是可变的。 但是我遇到的每个 Pythonista 都说并且继续说元组是不可变的,即使它们是不可散列的。 为什么?

从某种意义上说,元组是不可变的,因为元组中的对象不能被删除或被新对象替换。 就像 spam = 42; spam = 99 不改变 42 垃圾邮件中的对象; 它用一个全新的对象替换它, 99. 如果我们使用交互式 shell 查看包含列表的元组:

>>> spam = ('dogs', 'cats', [1, 2, 3])
>>> id(spam[0]), id(spam[1]), id(spam[2])
(41506216, 41355896, 40740488)

相同的对象将始终在此元组中,并且它们将始终以相同的顺序具有相同的身份:41506216、41355896 和 40740488。元组是不可变的。

然而,从另一种意义上说,元组是可变的,因为它们的值可以改变。 在交互式 shell 中输入以下内容:

>>> a = ('dogs', 'cats', [1, 2, 3])
>>> b = ('dogs', 'cats', [1, 2, 3])
>>> a == b
True
>>> a is b
False

在此示例中,由 ab 具有相等的值(根据 ==) 但它们是不同的对象(根据 is). 让我们更改列表 a的元组:

>>> a[2].append(99)
>>> a
('dogs', 'cats', [1, 2, 3, 99])
>>> a == b
False

我们改变了 a的价值。 我们必须有,因为 a 不再等于 b 我们没有改变 b的价值。 元组是可变的。

结语

“问题是,”Humpty Dumpty 说,“哪个是主人——仅此而已。”

人类是文字的主人,而不是相反。 文字是人类发明的,用来向其他人传达思想。 就个人而言,我将继续说元组是不可变的,因为这比不可变更准确,并且在大多数情况下都是最有用的术语。

然而,同时我们也应该意识到,从我们对“可变”和“值”的定义中,我们可以看出元组的值有时会发生变化,即变异。 说元组是可变的在技术上并没有错,尽管它肯定会引起人们的注意并需要更多的解释。

感谢您看完我关于元组和可变性的冗长解释。 我在写这篇文章的过程中确实学到了很多东西,希望我也能将它传达给你。

阅读更多

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注