为什么元组可以包含可变项?

211

如果一个元组是不可变的,为什么它可以包含可变项?

看起来似乎存在矛盾,因为当可变项(如列表)被修改时,它所属的元组仍然保持不可变。

8个回答

244

这是一个很好的问题。

关键的洞见是元组无法知道它们内部的对象是否可变。唯一使对象可变的方法就是拥有修改其数据的方法。通常情况下,没有办法检测到这一点。

另一个洞见是Python容器实际上并不包含任何内容。相反,它们保留对其他对象的引用。同样,Python变量不像编译语言中的变量;相反,变量名只是命名空间字典中的键,它们与相应的对象相关联。Ned Batchhelder在他的博客文章中很好地解释了这一点。无论哪种方式,对象只知道它们的引用计数;它们不知道这些引用是什么(变量、容器还是Python内部)。

综合这两个洞见可以解释你的谜团(为什么包含列表的不可变元组似乎在底层列表更改时发生了变化)。事实上,元组没有改变(它仍然具有与之前相同的其他对象的引用)。元组不能改变(因为它没有变异方法)。当列表更改时,元组没有收到更改的通知(列表不知道它是被变量、元组还是另一个列表所引用的)。

在这个话题上,以下是一些其他的想法,以帮助您完善对元组的认识,包括它们的工作原理和预期用途:

  1. 元组的特征不仅仅是它们的不可变性,更多的是它们的预期用途。
    元组是Python将异构信息收集到一个地方的方法。例如, s = ('www.python.org', 80) 将字符串和数字组合在一起,以便可以将主机/端口对作为套接字(一个组合对象)传递。从这个角度来看,拥有可变的组件是完全合理的。

  2. 不可变性与另一个属性hashability密切相关。但是,可哈希性并不是绝对的属性。如果元组的其中一个组件不可哈希,则整个元组也不可哈希。例如,t = ('red', [10, 20, 30]) 不可哈希。

最后一个示例展示了一个包含字符串和列表的2元组。元组本身不可变(即它没有任何更改其内容的方法)。同样,字符串是不可变的,因为字符串没有任何可变方法。列表对象具有可变方法,因此它可以被更改。这表明可变性是对象类型的属性--一些对象具有可变方法,而一些对象则没有。即使对象嵌套,这也不会改变。
记住两件事。首先,不可变性并不是魔法--它只是缺少可变方法。其次,对象不知道哪些变量或容器引用它们--它们只知道引用计数。
希望这对你有用 :-)

2
“元组无法知道其中的对象是否可变”这种说法不太准确。我们可以通过检测引用是否实现了哈希方法来判断其是否为不可变对象,比如字典或集合。这更多地取决于元组的设计目的,而非其本身的特性。 - garg10may
2
@garg10may 1) 没有调用hash()很难检测到可哈希性,因为从*object()*继承的所有内容都是可哈希的,因此子类需要明确关闭哈希。2) 可哈希性不能保证不可变性--很容易制作可变的可哈希对象的示例。3) 元组(像Python中的大多数容器一样)只是具有对底层对象的引用--它们没有责任检查它们并推断它们。 - Raymond Hettinger
@RaymondHettinger 谢谢你上面的精彩回答!你能在这里分享一下关于p2的例子吗?如果一个对象是可变的,我们如何对其进行哈希?我理解你在上面的回答中的观点,t = ('red', [10, 20, 30]) 不可哈希,因为它包含对一个可变元素的引用,尽管t本身是不可变的。在这个例子之后,我同意说“不可变性并不能保证可哈希性”。然而,我不明白你为什么说“可哈希性并不能保证不可变性”。 - undefined

183

8
这个问题存在歧义。这个答案充分解释了为什么元组可以包含可变对象,但没有解释为什么元组被设计成能够包含可变对象。我认为后者是更相关的问题。 - senderle

18
首先,对于不同的人来说,“不可变”这个词可能会有很多不同的含义。我特别喜欢埃里克·利珀特在他的博客文章[存档2012-03-12]中对不可变性进行分类的方式。在那里,他列出了以下几种不可变性:
  • Realio-trulio不可变性
  • 一次写入不可变性
  • Popsicle不可变性
  • 浅不可变性和深不可变性
  • 不可变外观
  • 观察不可变性
这些可以以各种方式组合,形成更多种类的不可变性,我相信还有更多存在。你似乎对深(也称为可传递)不可变性感兴趣,其中不可变对象只能包含其他不可变对象。
这里的关键点是深度不可变性只是众多不可变性中的一种。你可以选择任何一种你喜欢的不可变性,只要你意识到你对“不可变”的理解可能与其他人的不同。

1
Python元组具有哪种不可变性? - qazwsx
4
Python元组具有浅不可变性(也称为非传递性不可变性)。 - Ken Wayne VanderLinde

18

据我所知,这个问题需要重新表述为一个关于设计决策的问题:为什么Python的设计者选择创建一个不可变序列类型,它可以包含可变对象?

要回答这个问题,我们必须考虑到元组的用途:它们作为快速通用的序列。有了这个想法,很明显为什么元组是不可变的,但可以包含可变对象。换句话说:

  1. 元组是快速且内存效率高的:元组比列表更快地创建,因为它们是不可变的。不可变意味着元组可以作为常量创建并以此加载,使用常量折叠。这也意味着它们更快、更节省内存,因为无需过度分配等操作。对于随机项访问,它们比列表稍微慢一些,但在解包方面又更快(至少在我的机器上)。如果元组是可变的,那么对于这些目的,它们就不会像现在这样快。

  2. 元组是通用的:元组需要能够包含任何类型的对象。它们用于(快速)执行诸如可变长度参数列表(通过函数定义中的 * 运算符)。如果元组不能容纳可变对象,那么它们对于这类事情将毫无用处。Python 将不得不使用列表,这可能会减慢速度,并且肯定不会很节省内存。

所以你看,为了实现它们的目的,元组必须是不可变的,但也必须能够包含可变对象。如果Python的设计者想要创建一个保证其“包含”的所有对象都是不可变的不可变对象,他们将不得不创建第三个序列类型。这种收益并不值得额外的复杂性。

14

您不能更改其项目的id,因此它将始终包含相同的项目。

$ python
>>> t = (1, [2, 3])
>>> id(t[1])
12371368
>>> t[1].append(4)
>>> id(t[1])
12371368

这是上述示例的最恰当演示。元组引用那些不变的对象,尽管最多只有一个可变组件,但整个元组都是不可哈希的。 - piepi

5

我敢断言,这里关键的部分是,虽然你可以更改包含在元组中的列表或对象的状态,但你无法改变对象或列表的存在。如果你有一些依赖于thing [3]是一个列表的东西,即使是空的,那么我认为这很有用。


3

一个原因是Python中没有通用的方法将可变类型转换为不可变类型(请参见被拒绝的PEP 351,以及链接的讨论,了解其被拒绝的原因)。因此,如果有这种限制,将无法将各种类型的对象放入元组中,包括几乎所有用户创建的不可哈希对象。

字典和集合之所以具有此限制,仅是因为它们要求对象是可哈希的,因为它们在内部实现为哈希表。但请注意,具有此限制的是字典和集合本身,而不是不可变的(或可哈希的)。元组不使用对象的哈希值,因此其可变性并不重要。


2

元组是不可变的,这意味着元组本身不能扩展或缩小,但它包含的所有项本身并非都是不可变的。否则,元组就会很无聊。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接